diff --git a/apps/design_system_gallery/AGENTS.md b/apps/design_system_gallery/AGENTS.md index ea83a67..a12d24e 100644 --- a/apps/design_system_gallery/AGENTS.md +++ b/apps/design_system_gallery/AGENTS.md @@ -2,6 +2,31 @@ This document provides guidance for AI agents working on the Stream Design System Gallery (Widgetbook). +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Project Structure](#project-structure) +3. [Common Commands](#common-commands) +4. [Theme & Styling](#theme--styling) + - [Accessing Theme](#accessing-theme-in-use-cases-context-extensions) + - [Styling Guidelines](#styling-guidelines) + - [Keeping Material Theme in Sync](#keeping-material-theme-in-sync) +5. [Adding Content](#adding-content) + - [Adding Components](#adding-new-components) + - [Adding Semantic Tokens](#adding-semantic-token-showcases) + - [Adding Primitives](#adding-primitive-token-showcases) + - [Showcase Structure Patterns](#showcase-structure-patterns) + - [Category Ordering](#category-ordering) + - [Knobs Best Practices](#knobs-best-practices) +6. [Technical Details](#technical-details) + - [ThemeConfiguration](#themeconfiguration) + - [Preview Wrapper](#preview-wrapper) +7. [Troubleshooting](#troubleshooting) + +--- + ## Overview The gallery showcases Stream's design system components and foundation tokens. It uses: @@ -23,9 +48,13 @@ apps/design_system_gallery/ │ │ ├── stream_avatar.dart │ │ ├── stream_avatar_stack.dart │ │ └── stream_online_indicator.dart -│ ├── semantics/ # Semantic token showcases +│ ├── semantics/ # Semantic token showcases (design system level) │ │ ├── typography.dart # StreamTextTheme showcase │ │ └── elevations.dart # StreamBoxShadow showcase +│ ├── primitives/ # Primitive token showcases (raw values) +│ │ ├── radius.dart # StreamRadius showcase +│ │ ├── spacing.dart # StreamSpacing showcase +│ │ └── colors.dart # StreamColors showcase │ ├── config/ │ │ ├── theme_configuration.dart # Theme state (colors, brightness, etc.) │ │ └── preview_configuration.dart # Preview state (device, text scale) @@ -36,6 +65,156 @@ apps/design_system_gallery/ │ └── theme_studio/ # Theme customization panel widgets ``` +## Common Commands + +```bash +# Regenerate widgetbook directories (after adding/modifying use cases) +dart run build_runner build --delete-conflicting-outputs + +# Format code +dart format lib/ + +# Analyze +flutter analyze + +# Run gallery +flutter run -d chrome # or macos/windows +``` + +--- + +# Theme & Styling + +## Accessing Theme in Use Cases (Context Extensions) + +**Preferred:** Use context extensions for clean, concise access: + +```dart +// Recommended - use context extensions +final colorScheme = context.streamColorScheme; +final textTheme = context.streamTextTheme; +final boxShadow = context.streamBoxShadow; +final radius = context.streamRadius; +final spacing = context.streamSpacing; + +// For component themes +final avatarTheme = context.streamAvatarTheme; +final indicatorTheme = context.streamOnlineIndicatorTheme; +``` + +**Alternative:** Direct access via `StreamTheme.of(context)`: + +```dart +final streamTheme = StreamTheme.of(context); +final colorScheme = streamTheme.colorScheme; +final textTheme = streamTheme.textTheme; +``` + +## Styling Guidelines + +### Use context extensions (preferred) + +```dart +// Good - use context extensions +final colorScheme = context.streamColorScheme; +final textTheme = context.streamTextTheme; +style: textTheme.captionDefault.copyWith(color: colorScheme.textSecondary) + +// Avoid - passing parameters through widget tree +MyWidget({required this.colorScheme, required this.textTheme}) +``` + +### Don't pass theme data as parameters + +Widgets should access theme from context, not receive it as constructor parameters: + +```dart +// Good - access from context in build method +class _MyWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + return Text('Hello', style: textTheme.bodyDefault); + } +} + +// Bad - passing through constructor +class _MyWidget extends StatelessWidget { + final StreamColorScheme colorScheme; + final StreamTextTheme textTheme; + // ... +} +``` + +### Use StreamTheme tokens + +```dart +// Good +style: textTheme.captionDefault.copyWith(color: colorScheme.textSecondary) + +// Bad - hardcoded values +style: TextStyle(fontSize: 12, color: Colors.grey) +``` + +### Use StreamBoxShadow + +```dart +// Good +boxShadow: context.streamBoxShadow.elevation2 + +// Bad - custom shadows +boxShadow: [BoxShadow(blurRadius: 10, ...)] +``` + +### Use StreamRadius + +```dart +// Good +borderRadius: BorderRadius.all(context.streamRadius.md) + +// Bad - hardcoded values +borderRadius: BorderRadius.circular(8) +``` + +### Border handling + +Use `foregroundDecoration` for borders to prevent clipping: + +```dart +Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: colorScheme.backgroundSurface, + borderRadius: BorderRadius.circular(12), + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colorScheme.borderSurfaceSubtle), + ), + child: ..., +) +``` + +## Keeping Material Theme in Sync + +When modifying `ThemeConfiguration`, ensure `buildMaterialTheme()` stays updated: + +1. **New color properties** → Add to `ColorScheme` mapping and relevant theme components (buttons, inputs, dialogs, etc.) +2. **New text styles** → Update `TextTheme` mapping to use appropriate Stream styles +3. **New radius/spacing** → Update component themes that use borders/padding + +**Check these areas in `buildMaterialTheme()`:** +- `ColorScheme` - maps Stream colors to Material semantic colors +- `ThemeData` properties - `primaryColor`, `scaffoldBackgroundColor`, etc. +- Component themes - `dialogTheme`, `appBarTheme`, `filledButtonTheme`, etc. +- `TextTheme` - maps Stream text styles to Material text styles +- `extensions` - must include `[themeData]` for `StreamTheme.of(context)` to work + +--- + +# Adding Content + ## Adding New Components ### 1. Create Use Case File @@ -92,7 +271,7 @@ flutter analyze Semantic tokens (like `StreamTextTheme`, `StreamBoxShadow`) are showcased in `lib/semantics/`: 1. Create a new file in `lib/semantics/` -2. Use path `[App Foundation]/TokenName` in the `@UseCase` annotation +2. Use path `[App Foundation]/Semantics/TokenName` in the `@UseCase` annotation 3. Follow the same regeneration process as components **Example:** @@ -100,13 +279,50 @@ Semantic tokens (like `StreamTextTheme`, `StreamBoxShadow`) are showcased in `li @widgetbook.UseCase( name: 'All Styles', type: StreamTextTheme, - path: '[App Foundation]/Typography', + path: '[App Foundation]/Semantics/Typography', ) Widget buildStreamTextThemeShowcase(BuildContext context) { // Showcase implementation } ``` +## Adding Primitive Token Showcases + +Primitives (raw values like `StreamRadius`, `StreamSpacing`, `StreamColors`) are showcased in `lib/primitives/`: + +1. Create a new file in `lib/primitives/` +2. Use path `[App Foundation]/Primitives/TokenName` in the `@UseCase` annotation + +**Example:** +```dart +@widgetbook.UseCase( + name: 'Scale', + type: StreamRadius, + path: '[App Foundation]/Primitives/Radius', +) +Widget buildStreamRadiusShowcase(BuildContext context) { + // Showcase implementation +} +``` + +## Showcase Structure Patterns + +All primitives and semantics showcases follow a consistent structure. Reference existing files (`radius.dart`, `spacing.dart`, `typography.dart`, `elevations.dart`) for implementation examples. + +### Required Elements + +1. **`_SectionLabel` widget** - Accent-colored label for section headers (uppercase, letter-spacing) +2. **`_QuickReference` section** - Usage patterns and common choices at the bottom +3. **Token cards** - Visual preview (left) + info (right) layout + +### Card Styling Conventions + +- Token names: `accentPrimary` color, monospace font +- Value chips: `backgroundSurfaceSubtle` background, `textSecondary` color +- Usage descriptions: `textTertiary` color +- Borders: Use `foregroundDecoration` (not `border:` in BoxDecoration) +- Shadows: `boxShadow.elevation1` + ## Category Ordering The widgetbook generator sorts categories **alphabetically**. To control order, use prefixes: @@ -116,29 +332,20 @@ The widgetbook generator sorts categories **alphabetically**. To control order, **Current structure:** ``` -├── App Foundation (typography, elevations) -└── Components (avatar, button, etc.) -``` - -## Theme Configuration - -### Accessing Theme in Use Cases - -```dart -final streamTheme = StreamTheme.of(context); -final colorScheme = streamTheme.colorScheme; -final textTheme = streamTheme.textTheme; -final boxShadow = streamTheme.boxShadow; +├── App Foundation +│ ├── Primitives +│ │ ├── Colors +│ │ ├── Radius +│ │ └── Spacing +│ └── Semantics +│ ├── Elevations +│ └── Typography +└── Components + ├── Avatar (StreamAvatar, StreamAvatarStack) + ├── Button + └── Indicator (StreamOnlineIndicator) ``` -### Adding New Theme Properties - -1. Add private field and getter in `theme_configuration.dart` -2. Add setter using `_update()` pattern -3. Include in `_rebuildTheme()` colorScheme.copyWith() -4. Add to `resetToDefaults()` -5. Add UI control in `theme_customization_panel.dart` - ## Knobs Best Practices ### Do's @@ -150,94 +357,91 @@ final boxShadow = streamTheme.boxShadow; - Don't add knobs that duplicate Theme Studio controls - Don't use deprecated `context.knobs.list` (use `object.dropdown`) -## Preview Wrapper +--- -The `PreviewWrapper` applies: -- StreamTheme as a Material theme extension -- Device frame (optional) -- Text scale +# Technical Details -**Important:** StreamTheme is provided via `ThemeData.extensions` so `StreamTheme.of(context)` works correctly. +## ThemeConfiguration -## Color Picker Usage +### Accessing ThemeConfiguration -When using `flutter_colorpicker`, note that it may not rebuild correctly with `StatefulBuilder`. The current implementation uses a simple local variable approach: +Use `context.read()` for calling methods (no rebuild on change): ```dart -var pickerColor = initialColor; -// Don't wrap in StatefulBuilder - let ColorPicker manage its own state -ColorPicker( - pickerColor: pickerColor, - onColorChanged: (c) => pickerColor = c, -) +// For calling setters/methods - use read +context.read().setAccentPrimary(color); +context.read().resetToDefaults(); ``` -## Styling Guidelines +Use `context.watch()` only when you need to rebuild on changes (typically only in `gallery_app.dart`). -### Use StreamTheme tokens -```dart -// Good -style: textTheme.captionDefault.copyWith(color: colorScheme.textSecondary) +### Adding New Theme Properties -// Bad - hardcoded values -style: TextStyle(fontSize: 12, color: Colors.grey) -``` +1. Add private field and getter in `theme_configuration.dart` +2. Add setter using `_update()` pattern +3. Include in `_rebuildTheme()` colorScheme.copyWith() +4. Add to `resetToDefaults()` +5. Add UI control in `theme_customization_panel.dart` + +### Best Practices + +**Use class getters directly** in `buildMaterialTheme()`: -### Use StreamBoxShadow ```dart -// Good -boxShadow: streamTheme.boxShadow.elevation2 +// Good - uses class getters directly +ThemeData buildMaterialTheme() { + return ThemeData( + primaryColor: accentPrimary, // Class getter + scaffoldBackgroundColor: backgroundApp, // Class getter + ); +} -// Bad - custom shadows -boxShadow: [BoxShadow(blurRadius: 10, ...)] +// Avoid - unnecessary indirection +ThemeData buildMaterialTheme() { + final cs = themeData.colorScheme; + return ThemeData( + primaryColor: cs.accentPrimary, // Through colorScheme + ); +} ``` -### Border handling -Use `foregroundDecoration` for borders to prevent clipping: +**Use public getters** instead of private fields within the class: + ```dart -Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), - ), - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all(color: colorScheme.borderSurfaceSubtle), - ), - child: ..., -) -``` +// Good +final isDark = brightness == Brightness.dark; -## Common Commands +// Avoid +final isDark = _brightness == Brightness.dark; +``` -```bash -# Regenerate widgetbook directories -dart run build_runner build --delete-conflicting-outputs +## Preview Wrapper -# Format code -dart format lib/ +The `PreviewWrapper` applies: +- StreamTheme as a Material theme extension +- Device frame (optional) +- Text scale -# Analyze -flutter analyze +**Important:** StreamTheme is provided via `ThemeData.extensions` so `StreamTheme.of(context)` works correctly. -# Run gallery -flutter run -d chrome # or macos/windows -``` +--- ## Troubleshooting ### Theme changes not reflecting in use cases -Ensure `StreamTheme` is added to `ThemeData.extensions` in `PreviewWrapper`: +Ensure `StreamTheme` is added to `ThemeData.extensions`: ```dart -Theme( - data: ThemeData( - extensions: [streamTheme], // Required! - ), - child: ..., +ThemeData( + extensions: [streamTheme], // Required for StreamTheme.of(context)! ) ``` +This is done in `ThemeConfiguration.buildMaterialTheme()`. + ### Generated file has wrong order The generator sorts alphabetically. Use category name prefixes to control order (e.g., "App Foundation" before "Components"). +### Widgets not updating when theme changes +Ensure you're using context extensions (`context.streamColorScheme`) which properly depend on the inherited theme. Don't cache theme values in state. + + diff --git a/apps/design_system_gallery/assets/stream_logo.svg b/apps/design_system_gallery/assets/stream_logo.svg new file mode 100644 index 0000000..b0a5cca --- /dev/null +++ b/apps/design_system_gallery/assets/stream_logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/design_system_gallery/lib/app/gallery_app.dart b/apps/design_system_gallery/lib/app/gallery_app.dart index dd841e3..48a5c37 100644 --- a/apps/design_system_gallery/lib/app/gallery_app.dart +++ b/apps/design_system_gallery/lib/app/gallery_app.dart @@ -39,8 +39,8 @@ class _StreamDesignSystemGalleryState extends State { ], child: Consumer( builder: (context, themeConfig, _) { + final isDark = themeConfig.brightness == .dark; final materialTheme = themeConfig.buildMaterialTheme(); - final isDark = themeConfig.brightness == Brightness.dark; return MaterialApp( title: 'Stream Design System', @@ -48,7 +48,7 @@ class _StreamDesignSystemGalleryState extends State { // Use Stream-themed Material theme for all regular widgets theme: materialTheme, darkTheme: materialTheme, - themeMode: isDark ? ThemeMode.dark : ThemeMode.light, + themeMode: isDark ? .dark : .light, home: GalleryShell( showThemePanel: _showThemePanel, onToggleThemePanel: () => setState(() => _showThemePanel = !_showThemePanel), 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 bc646e7..55d133c 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 @@ -10,14 +10,18 @@ // ************************************************************************** // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:design_system_gallery/components/button.dart' - as _design_system_gallery_components_button; import 'package:design_system_gallery/components/stream_avatar.dart' as _design_system_gallery_components_stream_avatar; import 'package:design_system_gallery/components/stream_avatar_stack.dart' as _design_system_gallery_components_stream_avatar_stack; import 'package:design_system_gallery/components/stream_online_indicator.dart' as _design_system_gallery_components_stream_online_indicator; +import 'package:design_system_gallery/primitives/colors.dart' + as _design_system_gallery_primitives_colors; +import 'package:design_system_gallery/primitives/radius.dart' + as _design_system_gallery_primitives_radius; +import 'package:design_system_gallery/primitives/spacing.dart' + as _design_system_gallery_primitives_spacing; import 'package:design_system_gallery/semantics/elevations.dart' as _design_system_gallery_semantics_elevations; import 'package:design_system_gallery/semantics/typography.dart' @@ -29,30 +33,85 @@ final directories = <_widgetbook.WidgetbookNode>[ name: 'App Foundation', children: [ _widgetbook.WidgetbookFolder( - name: 'Elevations', + name: 'Primitives', children: [ - _widgetbook.WidgetbookComponent( - name: 'StreamBoxShadow', - useCases: [ - _widgetbook.WidgetbookUseCase( - name: 'All Elevations', - builder: _design_system_gallery_semantics_elevations - .buildStreamBoxShadowShowcase, + _widgetbook.WidgetbookFolder( + name: 'Colors', + children: [ + _widgetbook.WidgetbookComponent( + name: 'StreamColors', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'All Colors', + builder: _design_system_gallery_primitives_colors + .buildStreamColorsShowcase, + ), + ], + ), + ], + ), + _widgetbook.WidgetbookFolder( + name: 'Radius', + children: [ + _widgetbook.WidgetbookComponent( + name: 'StreamRadius', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'All Values', + builder: _design_system_gallery_primitives_radius + .buildStreamRadiusShowcase, + ), + ], + ), + ], + ), + _widgetbook.WidgetbookFolder( + name: 'Spacing', + children: [ + _widgetbook.WidgetbookComponent( + name: 'StreamSpacing', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'All Values', + builder: _design_system_gallery_primitives_spacing + .buildStreamSpacingShowcase, + ), + ], ), ], ), ], ), _widgetbook.WidgetbookFolder( - name: 'Typography', + name: 'Semantics', children: [ - _widgetbook.WidgetbookComponent( - name: 'StreamTextTheme', - useCases: [ - _widgetbook.WidgetbookUseCase( - name: 'All Styles', - builder: _design_system_gallery_semantics_typography - .buildStreamTextThemeShowcase, + _widgetbook.WidgetbookFolder( + name: 'Elevations', + children: [ + _widgetbook.WidgetbookComponent( + name: 'StreamBoxShadow', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'All Elevations', + builder: _design_system_gallery_semantics_elevations + .buildStreamBoxShadowShowcase, + ), + ], + ), + ], + ), + _widgetbook.WidgetbookFolder( + name: 'Typography', + children: [ + _widgetbook.WidgetbookComponent( + name: 'StreamTextTheme', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'All Styles', + builder: _design_system_gallery_semantics_typography + .buildStreamTextThemeShowcase, + ), + ], ), ], ), @@ -69,25 +128,15 @@ final directories = <_widgetbook.WidgetbookNode>[ _widgetbook.WidgetbookComponent( name: 'StreamAvatar', useCases: [ - _widgetbook.WidgetbookUseCase( - name: 'Color Palette', - builder: _design_system_gallery_components_stream_avatar - .buildStreamAvatarPalette, - ), _widgetbook.WidgetbookUseCase( name: 'Playground', builder: _design_system_gallery_components_stream_avatar .buildStreamAvatarPlayground, ), _widgetbook.WidgetbookUseCase( - name: 'Real-world Example', + name: 'Showcase', builder: _design_system_gallery_components_stream_avatar - .buildStreamAvatarExample, - ), - _widgetbook.WidgetbookUseCase( - name: 'Size Variants', - builder: _design_system_gallery_components_stream_avatar - .buildStreamAvatarSizes, + .buildStreamAvatarShowcase, ), ], ), @@ -100,44 +149,9 @@ final directories = <_widgetbook.WidgetbookNode>[ .buildStreamAvatarStackPlayground, ), _widgetbook.WidgetbookUseCase( - name: 'Real-world Example', + name: 'Showcase', builder: _design_system_gallery_components_stream_avatar_stack - .buildStreamAvatarStackExample, - ), - ], - ), - ], - ), - _widgetbook.WidgetbookFolder( - name: 'Button', - children: [ - _widgetbook.WidgetbookComponent( - name: 'StreamButton', - useCases: [ - _widgetbook.WidgetbookUseCase( - name: 'Playground', - builder: _design_system_gallery_components_button - .buildStreamButtonPlayground, - ), - _widgetbook.WidgetbookUseCase( - name: 'Real-world Example', - builder: _design_system_gallery_components_button - .buildStreamButtonExample, - ), - _widgetbook.WidgetbookUseCase( - name: 'Size Variants', - builder: _design_system_gallery_components_button - .buildStreamButtonSizes, - ), - _widgetbook.WidgetbookUseCase( - name: 'Type Variants', - builder: _design_system_gallery_components_button - .buildStreamButtonTypes, - ), - _widgetbook.WidgetbookUseCase( - name: 'With Icons', - builder: _design_system_gallery_components_button - .buildStreamButtonWithIcons, + .buildStreamAvatarStackShowcase, ), ], ), @@ -156,16 +170,10 @@ final directories = <_widgetbook.WidgetbookNode>[ .buildStreamOnlineIndicatorPlayground, ), _widgetbook.WidgetbookUseCase( - name: 'Real-world Example', - builder: - _design_system_gallery_components_stream_online_indicator - .buildStreamOnlineIndicatorExample, - ), - _widgetbook.WidgetbookUseCase( - name: 'Size Variants', + name: 'Showcase', builder: _design_system_gallery_components_stream_online_indicator - .buildStreamOnlineIndicatorSizes, + .buildStreamOnlineIndicatorShowcase, ), ], ), diff --git a/apps/design_system_gallery/lib/app/gallery_shell.dart b/apps/design_system_gallery/lib/app/gallery_shell.dart index 2bd1e0d..940837d 100644 --- a/apps/design_system_gallery/lib/app/gallery_shell.dart +++ b/apps/design_system_gallery/lib/app/gallery_shell.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; import 'package:widgetbook/widgetbook.dart'; -import '../config/theme_configuration.dart'; import '../core/preview_wrapper.dart'; import '../widgets/theme_studio/theme_customization_panel.dart'; import '../widgets/toolbar/toolbar.dart'; import 'gallery_app.directories.g.dart'; +import 'home.dart'; /// The main shell that wraps the widgetbook with custom Stream branding. /// @@ -26,11 +26,11 @@ class GalleryShell extends StatelessWidget { @override Widget build(BuildContext context) { - final themeConfig = context.watch(); - final isDark = themeConfig.brightness == Brightness.dark; + final materialTheme = Theme.of(context); + final isDark = materialTheme.brightness == .dark; return Scaffold( - backgroundColor: _getShellBackground(themeConfig.brightness), + backgroundColor: context.streamColorScheme.backgroundApp, body: Column( children: [ // Toolbar spans across the entire width @@ -45,21 +45,21 @@ class GalleryShell extends StatelessWidget { // Widgetbook area Expanded( child: Widgetbook.material( - // Theme updates via themeMode/lightTheme/darkTheme props - // Preview updates via PreviewWrapper's ListenableBuilder - themeMode: isDark ? ThemeMode.dark : ThemeMode.light, - lightTheme: themeConfig.buildMaterialTheme(), - darkTheme: themeConfig.buildMaterialTheme(), + lightTheme: materialTheme, + darkTheme: materialTheme, + themeMode: isDark ? .dark : .light, directories: directories, + home: const GalleryHomePage(), appBuilder: (context, child) => PreviewWrapper(child: child), ), ), - // Theme customization panel - aligned with widgetbook content - if (showThemePanel) - SizedBox( + // Theme customization panel + if (showThemePanel) ...[ + const SizedBox( width: 340, - child: ThemeCustomizationPanel(configuration: themeConfig), + child: ThemeCustomizationPanel(), ), + ], ], ), ), @@ -67,8 +67,4 @@ class GalleryShell extends StatelessWidget { ), ); } - - Color _getShellBackground(Brightness brightness) { - return brightness == Brightness.dark ? const Color(0xFF0A0A0A) : const Color(0xFFF8F9FA); - } } diff --git a/apps/design_system_gallery/lib/app/home.dart b/apps/design_system_gallery/lib/app/home.dart new file mode 100644 index 0000000..083c6ca --- /dev/null +++ b/apps/design_system_gallery/lib/app/home.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; +import 'package:svg_icon_widget/svg_icon_widget.dart'; + +import '../core/stream_icons.dart'; + +/// The home page content for the gallery. +/// Displayed when no component is selected in the sidebar. +class GalleryHomePage extends StatelessWidget { + const GalleryHomePage({super.key}); + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return ColoredBox( + color: colorScheme.backgroundApp, + child: Center( + child: SingleChildScrollView( + padding: EdgeInsets.all(spacing.xl), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 560), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo + const _StreamLogo(), + SizedBox(height: spacing.xl), + + // Title + Text( + 'Stream Design System', + style: textTheme.headingLg.copyWith( + color: colorScheme.textPrimary, + fontWeight: FontWeight.w700, + letterSpacing: -0.5, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: spacing.sm), + + // Subtitle + Text( + 'A comprehensive design system for building beautiful, ' + 'consistent chat and activity feed experiences.', + style: textTheme.bodyDefault.copyWith( + color: colorScheme.textSecondary, + height: 1.5, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: spacing.xl), + + // Feature chips + const _FeatureChips(), + SizedBox(height: spacing.xl + spacing.lg), + + // Getting started hint + const _GettingStartedHint(), + ], + ), + ), + ), + ), + ); + } +} + +class _StreamLogo extends StatelessWidget { + const _StreamLogo(); + + @override + Widget build(BuildContext context) { + return const SvgIcon( + StreamIcons.logo, + size: 80, + ); + } +} + +class _FeatureChips extends StatelessWidget { + const _FeatureChips(); + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + return Wrap( + spacing: spacing.sm, + runSpacing: spacing.sm, + alignment: WrapAlignment.center, + children: const [ + _FeatureChip(icon: Icons.palette_outlined, label: 'Themeable'), + _FeatureChip(icon: Icons.dark_mode_outlined, label: 'Dark Mode'), + _FeatureChip(icon: Icons.devices_outlined, label: 'Responsive'), + _FeatureChip(icon: Icons.accessibility_new_outlined, label: 'Accessible'), + ], + ); + } +} + +class _FeatureChip extends StatelessWidget { + const _FeatureChip({ + required this.icon, + required this.label, + }); + + final IconData icon; + 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.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.md), + border: Border.all(color: colorScheme.borderSurfaceSubtle), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 16, + color: colorScheme.textSecondary, + ), + SizedBox(width: spacing.xs), + Text( + label, + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + ], + ), + ); + } +} + +class _GettingStartedHint extends StatelessWidget { + const _GettingStartedHint(); + + @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.all(spacing.md), + decoration: BoxDecoration( + color: colorScheme.accentPrimary.withValues(alpha: 0.08), + borderRadius: BorderRadius.all(radius.lg), + border: Border.all( + color: colorScheme.accentPrimary.withValues(alpha: 0.2), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.arrow_back, + size: 18, + color: colorScheme.accentPrimary, + ), + SizedBox(width: spacing.sm), + Text( + 'Select a component from the sidebar to get started', + style: textTheme.captionDefault.copyWith( + color: colorScheme.accentPrimary, + ), + ), + ], + ), + ); + } +} diff --git a/apps/design_system_gallery/lib/components/button.dart b/apps/design_system_gallery/lib/components/button.dart index cf0fb6e..9974881 100644 --- a/apps/design_system_gallery/lib/components/button.dart +++ b/apps/design_system_gallery/lib/components/button.dart @@ -1,389 +1,389 @@ -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: StreamButton, - path: '[Components]/Button', -) -Widget buildStreamButtonPlayground(BuildContext context) { - final label = context.knobs.string( - label: 'Label', - initialValue: 'Click me', - description: 'The text displayed on the button.', - ); - - final type = context.knobs.object.dropdown( - label: 'Type', - options: StreamButtonType.values, - initialOption: StreamButtonType.primary, - labelBuilder: (option) => option.name, - description: 'Button visual style variant.', - ); - - final size = context.knobs.object.dropdown( - label: 'Size', - options: StreamButtonSize.values, - initialOption: StreamButtonSize.large, - labelBuilder: (option) => option.name, - description: 'Button size preset (affects padding and font size).', - ); - - final isDisabled = context.knobs.boolean( - label: 'Disabled', - description: 'Whether the button is disabled (non-interactive).', - ); - - final showLeadingIcon = context.knobs.boolean( - label: 'Leading Icon', - description: 'Show an icon before the label.', - ); - - final showTrailingIcon = context.knobs.boolean( - label: 'Trailing Icon', - description: 'Show an icon after the label.', - ); - - return Center( - child: StreamButton( - label: label, - type: type, - size: size, - onTap: isDisabled - ? null - : () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Button tapped!'), - duration: Duration(seconds: 1), - ), - ); - }, - iconLeft: showLeadingIcon ? const Icon(Icons.add) : null, - iconRight: showTrailingIcon ? const Icon(Icons.arrow_forward) : null, - ), - ); -} - -// ============================================================================= -// Type Variants -// ============================================================================= - -@widgetbook.UseCase( - name: 'Type Variants', - type: StreamButton, - path: '[Components]/Button', -) -Widget buildStreamButtonTypes(BuildContext context) { - final theme = StreamTheme.of(context); - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; - - return Center( - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (final type in StreamButtonType.values) ...[ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 100, - child: Text( - type.name, - style: textTheme.captionDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - ), - StreamButton( - label: 'Button', - type: type, - onTap: () {}, - ), - ], - ), - if (type != StreamButtonType.values.last) const SizedBox(height: 16), - ], - ], - ), - ), - ); -} - -// ============================================================================= -// Size Variants -// ============================================================================= - -@widgetbook.UseCase( - name: 'Size Variants', - type: StreamButton, - path: '[Components]/Button', -) -Widget buildStreamButtonSizes(BuildContext context) { - final theme = StreamTheme.of(context); - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; - - return Center( - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (final size in StreamButtonSize.values) ...[ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 80, - child: Text( - size.name, - style: textTheme.captionDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - ), - StreamButton( - label: 'Button', - size: size, - onTap: () {}, - ), - ], - ), - if (size != StreamButtonSize.values.last) const SizedBox(height: 16), - ], - ], - ), - ), - ); -} - -// ============================================================================= -// With Icons -// ============================================================================= - -@widgetbook.UseCase( - name: 'With Icons', - type: StreamButton, - path: '[Components]/Button', -) -Widget buildStreamButtonWithIcons(BuildContext context) { - final theme = StreamTheme.of(context); - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; - - return Center( - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 80, - child: Text( - 'Leading', - style: textTheme.captionDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - ), - StreamButton( - label: 'Add Item', - iconLeft: const Icon(Icons.add), - onTap: () {}, - ), - ], - ), - const SizedBox(height: 16), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 80, - child: Text( - 'Trailing', - style: textTheme.captionDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - ), - StreamButton( - label: 'Continue', - iconRight: const Icon(Icons.arrow_forward), - onTap: () {}, - ), - ], - ), - const SizedBox(height: 16), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 80, - child: Text( - 'Both', - style: textTheme.captionDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - ), - StreamButton( - label: 'Upload', - iconLeft: const Icon(Icons.cloud_upload), - iconRight: const Icon(Icons.arrow_forward), - onTap: () {}, - ), - ], - ), - ], - ), - ), - ); -} - -// ============================================================================= -// Real-world Example -// ============================================================================= - -@widgetbook.UseCase( - name: 'Real-world Example', - type: StreamButton, - path: '[Components]/Button', -) -Widget buildStreamButtonExample(BuildContext context) { - final theme = StreamTheme.of(context); - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; - - return Center( - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Common Patterns', - style: textTheme.headingSm.copyWith( - color: colorScheme.textPrimary, - ), - ), - const SizedBox(height: 16), - // Dialog actions - Container( - padding: const EdgeInsets.all(16), - width: 280, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), - ), - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: colorScheme.borderSurfaceSubtle), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Delete conversation?', - style: textTheme.bodyEmphasis.copyWith( - color: colorScheme.textPrimary, - ), - ), - const SizedBox(height: 4), - Text( - 'This action cannot be undone.', - style: textTheme.captionDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - StreamButton( - label: 'Cancel', - type: StreamButtonType.secondary, - size: StreamButtonSize.small, - onTap: () {}, - ), - const SizedBox(width: 8), - StreamButton( - label: 'Delete', - type: StreamButtonType.destructive, - size: StreamButtonSize.small, - onTap: () {}, - ), - ], - ), - ], - ), - ), - const SizedBox(height: 12), - // Form submit - Container( - padding: const EdgeInsets.all(16), - width: 280, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), - ), - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: colorScheme.borderSurfaceSubtle), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Ready to send?', - style: textTheme.bodyEmphasis.copyWith( - color: colorScheme.textPrimary, - ), - ), - const SizedBox(height: 12), - StreamButton( - label: 'Send Message', - iconRight: const Icon(Icons.send), - onTap: () {}, - ), - ], - ), - ), - ], - ), - ), - ); -} +// 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: StreamButton, +// path: '[Components]/Button', +// ) +// Widget buildStreamButtonPlayground(BuildContext context) { +// final label = context.knobs.string( +// label: 'Label', +// initialValue: 'Click me', +// description: 'The text displayed on the button.', +// ); +// +// final type = context.knobs.object.dropdown( +// label: 'Type', +// options: StreamButtonType.values, +// initialOption: StreamButtonType.primary, +// labelBuilder: (option) => option.name, +// description: 'Button visual style variant.', +// ); +// +// final size = context.knobs.object.dropdown( +// label: 'Size', +// options: StreamButtonSize.values, +// initialOption: StreamButtonSize.large, +// labelBuilder: (option) => option.name, +// description: 'Button size preset (affects padding and font size).', +// ); +// +// final isDisabled = context.knobs.boolean( +// label: 'Disabled', +// description: 'Whether the button is disabled (non-interactive).', +// ); +// +// final showLeadingIcon = context.knobs.boolean( +// label: 'Leading Icon', +// description: 'Show an icon before the label.', +// ); +// +// final showTrailingIcon = context.knobs.boolean( +// label: 'Trailing Icon', +// description: 'Show an icon after the label.', +// ); +// +// return Center( +// child: StreamButton( +// label: label, +// type: type, +// size: size, +// onTap: isDisabled +// ? null +// : () { +// ScaffoldMessenger.of(context).showSnackBar( +// const SnackBar( +// content: Text('Button tapped!'), +// duration: Duration(seconds: 1), +// ), +// ); +// }, +// iconLeft: showLeadingIcon ? const Icon(Icons.add) : null, +// iconRight: showTrailingIcon ? const Icon(Icons.arrow_forward) : null, +// ), +// ); +// } +// +// // ============================================================================= +// // Type Variants +// // ============================================================================= +// +// @widgetbook.UseCase( +// name: 'Type Variants', +// type: StreamButton, +// path: '[Components]/Button', +// ) +// Widget buildStreamButtonTypes(BuildContext context) { +// final theme = StreamTheme.of(context); +// final colorScheme = theme.colorScheme; +// final textTheme = theme.textTheme; +// +// return Center( +// child: Container( +// padding: const EdgeInsets.all(24), +// decoration: BoxDecoration( +// color: colorScheme.backgroundSurface, +// borderRadius: BorderRadius.circular(12), +// ), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// for (final type in StreamButtonType.values) ...[ +// Row( +// mainAxisSize: MainAxisSize.min, +// children: [ +// SizedBox( +// width: 100, +// child: Text( +// type.name, +// style: textTheme.captionDefault.copyWith( +// color: colorScheme.textSecondary, +// ), +// ), +// ), +// StreamButton( +// label: 'Button', +// type: type, +// onTap: () {}, +// ), +// ], +// ), +// if (type != StreamButtonType.values.last) const SizedBox(height: 16), +// ], +// ], +// ), +// ), +// ); +// } +// +// // ============================================================================= +// // Size Variants +// // ============================================================================= +// +// @widgetbook.UseCase( +// name: 'Size Variants', +// type: StreamButton, +// path: '[Components]/Button', +// ) +// Widget buildStreamButtonSizes(BuildContext context) { +// final theme = StreamTheme.of(context); +// final colorScheme = theme.colorScheme; +// final textTheme = theme.textTheme; +// +// return Center( +// child: Container( +// padding: const EdgeInsets.all(24), +// decoration: BoxDecoration( +// color: colorScheme.backgroundSurface, +// borderRadius: BorderRadius.circular(12), +// ), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// for (final size in StreamButtonSize.values) ...[ +// Row( +// mainAxisSize: MainAxisSize.min, +// children: [ +// SizedBox( +// width: 80, +// child: Text( +// size.name, +// style: textTheme.captionDefault.copyWith( +// color: colorScheme.textSecondary, +// ), +// ), +// ), +// StreamButton( +// label: 'Button', +// size: size, +// onTap: () {}, +// ), +// ], +// ), +// if (size != StreamButtonSize.values.last) const SizedBox(height: 16), +// ], +// ], +// ), +// ), +// ); +// } +// +// // ============================================================================= +// // With Icons +// // ============================================================================= +// +// @widgetbook.UseCase( +// name: 'With Icons', +// type: StreamButton, +// path: '[Components]/Button', +// ) +// Widget buildStreamButtonWithIcons(BuildContext context) { +// final theme = StreamTheme.of(context); +// final colorScheme = theme.colorScheme; +// final textTheme = theme.textTheme; +// +// return Center( +// child: Container( +// padding: const EdgeInsets.all(24), +// decoration: BoxDecoration( +// color: colorScheme.backgroundSurface, +// borderRadius: BorderRadius.circular(12), +// ), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// Row( +// mainAxisSize: MainAxisSize.min, +// children: [ +// SizedBox( +// width: 80, +// child: Text( +// 'Leading', +// style: textTheme.captionDefault.copyWith( +// color: colorScheme.textSecondary, +// ), +// ), +// ), +// StreamButton( +// label: 'Add Item', +// iconLeft: const Icon(Icons.add), +// onTap: () {}, +// ), +// ], +// ), +// const SizedBox(height: 16), +// Row( +// mainAxisSize: MainAxisSize.min, +// children: [ +// SizedBox( +// width: 80, +// child: Text( +// 'Trailing', +// style: textTheme.captionDefault.copyWith( +// color: colorScheme.textSecondary, +// ), +// ), +// ), +// StreamButton( +// label: 'Continue', +// iconRight: const Icon(Icons.arrow_forward), +// onTap: () {}, +// ), +// ], +// ), +// const SizedBox(height: 16), +// Row( +// mainAxisSize: MainAxisSize.min, +// children: [ +// SizedBox( +// width: 80, +// child: Text( +// 'Both', +// style: textTheme.captionDefault.copyWith( +// color: colorScheme.textSecondary, +// ), +// ), +// ), +// StreamButton( +// label: 'Upload', +// iconLeft: const Icon(Icons.cloud_upload), +// iconRight: const Icon(Icons.arrow_forward), +// onTap: () {}, +// ), +// ], +// ), +// ], +// ), +// ), +// ); +// } +// +// // ============================================================================= +// // Real-world Example +// // ============================================================================= +// +// @widgetbook.UseCase( +// name: 'Real-world Example', +// type: StreamButton, +// path: '[Components]/Button', +// ) +// Widget buildStreamButtonExample(BuildContext context) { +// final theme = StreamTheme.of(context); +// final colorScheme = theme.colorScheme; +// final textTheme = theme.textTheme; +// +// return Center( +// child: Container( +// padding: const EdgeInsets.all(24), +// decoration: BoxDecoration( +// color: colorScheme.backgroundSurface, +// borderRadius: BorderRadius.circular(12), +// ), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// 'Common Patterns', +// style: textTheme.headingSm.copyWith( +// color: colorScheme.textPrimary, +// ), +// ), +// const SizedBox(height: 16), +// // Dialog actions +// Container( +// padding: const EdgeInsets.all(16), +// width: 280, +// clipBehavior: Clip.antiAlias, +// decoration: BoxDecoration( +// color: colorScheme.backgroundApp, +// borderRadius: BorderRadius.circular(8), +// ), +// foregroundDecoration: BoxDecoration( +// borderRadius: BorderRadius.circular(8), +// border: Border.all(color: colorScheme.borderSurfaceSubtle), +// ), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// mainAxisSize: MainAxisSize.min, +// children: [ +// Text( +// 'Delete conversation?', +// style: textTheme.bodyEmphasis.copyWith( +// color: colorScheme.textPrimary, +// ), +// ), +// const SizedBox(height: 4), +// Text( +// 'This action cannot be undone.', +// style: textTheme.captionDefault.copyWith( +// color: colorScheme.textSecondary, +// ), +// ), +// const SizedBox(height: 16), +// Row( +// mainAxisAlignment: MainAxisAlignment.end, +// children: [ +// StreamButton( +// label: 'Cancel', +// type: StreamButtonType.secondary, +// size: StreamButtonSize.small, +// onTap: () {}, +// ), +// const SizedBox(width: 8), +// StreamButton( +// label: 'Delete', +// type: StreamButtonType.destructive, +// size: StreamButtonSize.small, +// onTap: () {}, +// ), +// ], +// ), +// ], +// ), +// ), +// const SizedBox(height: 12), +// // Form submit +// Container( +// padding: const EdgeInsets.all(16), +// width: 280, +// clipBehavior: Clip.antiAlias, +// decoration: BoxDecoration( +// color: colorScheme.backgroundApp, +// borderRadius: BorderRadius.circular(8), +// ), +// foregroundDecoration: BoxDecoration( +// borderRadius: BorderRadius.circular(8), +// border: Border.all(color: colorScheme.borderSurfaceSubtle), +// ), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.stretch, +// mainAxisSize: MainAxisSize.min, +// children: [ +// Text( +// 'Ready to send?', +// style: textTheme.bodyEmphasis.copyWith( +// color: colorScheme.textPrimary, +// ), +// ), +// const SizedBox(height: 12), +// StreamButton( +// label: 'Send Message', +// iconRight: const Icon(Icons.send), +// onTap: () {}, +// ), +// ], +// ), +// ), +// ], +// ), +// ), +// ); +// } diff --git a/apps/design_system_gallery/lib/components/stream_avatar.dart b/apps/design_system_gallery/lib/components/stream_avatar.dart index 9768c7c..e3f8e8b 100644 --- a/apps/design_system_gallery/lib/components/stream_avatar.dart +++ b/apps/design_system_gallery/lib/components/stream_avatar.dart @@ -54,253 +54,510 @@ Widget buildStreamAvatarPlayground(BuildContext context) { } // ============================================================================= -// Size Variants +// Showcase // ============================================================================= @widgetbook.UseCase( - name: 'Size Variants', + name: 'Showcase', type: StreamAvatar, path: '[Components]/Avatar', ) -Widget buildStreamAvatarSizes(BuildContext context) { - final theme = StreamTheme.of(context); - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; +Widget buildStreamAvatarShowcase(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; - return Center( - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, + return DefaultTextStyle( + style: textTheme.bodyDefault.copyWith(color: colorScheme.textPrimary), + child: SingleChildScrollView( + padding: EdgeInsets.all(spacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - for (final size in StreamAvatarSize.values) ...[ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - StreamAvatar( + // Size variants + const _SizeVariantsSection(), + SizedBox(height: spacing.xl), + + // Color palette + const _PaletteSection(), + SizedBox(height: spacing.xl), + + // Usage patterns + const _UsagePatternsSection(), + ], + ), + ), + ); +} + +// ============================================================================= +// Size Variants Section +// ============================================================================= + +class _SizeVariantsSection extends StatelessWidget { + const _SizeVariantsSection(); + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'SIZE SCALE'), + SizedBox(height: spacing.md), + ...StreamAvatarSize.values.map((size) => _SizeCard(size: size)), + ], + ); + } +} + +class _SizeCard extends StatelessWidget { + const _SizeCard({required this.size}); + + final StreamAvatarSize size; + + String _getUsage(StreamAvatarSize size) { + return switch (size) { + StreamAvatarSize.xs => 'Compact lists, inline mentions', + StreamAvatarSize.sm => 'Chat list items, notifications', + StreamAvatarSize.md => 'Message bubbles, comments', + StreamAvatarSize.lg => 'Profile headers, user cards', + }; + } + + @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 Padding( + padding: EdgeInsets.only(bottom: spacing.sm), + child: Container( + 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.borderSurfaceSubtle), + ), + child: Row( + children: [ + // Avatar preview + SizedBox( + width: 80, + height: 80, + child: Center( + child: StreamAvatar( imageUrl: _sampleImageUrl, size: size, placeholder: (context) => const Text('AB'), ), - const SizedBox(height: 8), - Text( - size.name.toUpperCase(), - style: textTheme.captionEmphasis.copyWith( - color: colorScheme.textPrimary, + ), + ), + SizedBox(width: spacing.md + spacing.xs), + // Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'StreamAvatarSize.${size.name}', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + SizedBox(width: spacing.sm), + Container( + padding: EdgeInsets.symmetric( + horizontal: spacing.xs + spacing.xxs, + vertical: spacing.xxs, + ), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.xs), + ), + child: Text( + '${size.value.toInt()}px', + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.textSecondary, + fontFamily: 'monospace', + fontSize: 10, + ), + ), + ), + ], ), - ), - Text( - '${size.value.toInt()}px', - style: textTheme.metadataDefault.copyWith( - color: colorScheme.textTertiary, + SizedBox(height: spacing.xs + spacing.xxs), + Text( + _getUsage(size), + style: textTheme.captionDefault.copyWith( + color: colorScheme.textTertiary, + ), ), - ), - ], + ], + ), ), - if (size != StreamAvatarSize.values.last) const SizedBox(width: 24), ], - ], + ), ), - ), - ); + ); + } } // ============================================================================= -// Color Palette +// Palette Section // ============================================================================= -@widgetbook.UseCase( - name: 'Color Palette', - type: StreamAvatar, - path: '[Components]/Avatar', -) -Widget buildStreamAvatarPalette(BuildContext context) { - final theme = StreamTheme.of(context); - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; - final palette = colorScheme.avatarPalette; +class _PaletteSection extends StatelessWidget { + const _PaletteSection(); - return Center( - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Theme Avatar Palette', - style: textTheme.headingSm.copyWith( - color: colorScheme.textPrimary, - ), + @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; + final palette = colorScheme.avatarPalette; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'COLOR PALETTE'), + 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, ), - Text( - 'Automatically assigned based on user ID', - style: textTheme.captionDefault.copyWith( - color: colorScheme.textTertiary, - ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), + border: Border.all(color: colorScheme.borderSurfaceSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Theme-defined color pairs for placeholder avatars', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + Wrap( + spacing: spacing.sm, + runSpacing: spacing.sm, + children: [ + for (var i = 0; i < palette.length; i++) _PaletteItem(index: i, entry: palette[i]), + ], + ), + SizedBox(height: spacing.md), + Divider(color: colorScheme.borderSurfaceSubtle), + SizedBox(height: spacing.sm), + Row( + children: [ + Icon( + Icons.info_outline, + size: 14, + color: colorScheme.textTertiary, + ), + SizedBox(width: spacing.xs + spacing.xxs), + Expanded( + child: Text( + 'Colors are automatically assigned based on user ID hash', + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ); + } +} + +class _PaletteItem extends StatelessWidget { + const _PaletteItem({required this.index, required this.entry}); + + final int index; + final StreamAvatarColorPair entry; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + StreamAvatar( + size: StreamAvatarSize.lg, + backgroundColor: entry.backgroundColor, + foregroundColor: entry.foregroundColor, + placeholder: (context) => Text(_getInitials(index)), + ), + SizedBox(height: spacing.xs + spacing.xxs), + Text( + 'palette[$index]', + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + fontFamily: 'monospace', + fontSize: 10, + ), + ), + ], + ); + } +} + +// ============================================================================= +// Usage Patterns Section +// ============================================================================= + +class _UsagePatternsSection extends StatelessWidget { + const _UsagePatternsSection(); + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + final palette = colorScheme.avatarPalette; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'USAGE PATTERNS'), + SizedBox(height: spacing.md), + + // Profile header example + _ExampleCard( + title: 'Profile Header', + description: 'Large avatar with user details', + child: Row( + children: [ + StreamAvatar( + imageUrl: _sampleImageUrl, + size: StreamAvatarSize.lg, + placeholder: (context) => const Text('JD'), + ), + SizedBox(width: spacing.md), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Jane Doe', + style: textTheme.headingSm.copyWith( + color: colorScheme.textPrimary, + ), + ), + Text( + 'Product Designer', + style: textTheme.bodyDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + ], + ), + ], ), - const SizedBox(height: 16), - Wrap( - spacing: 16, - runSpacing: 16, + ), + SizedBox(height: spacing.sm), + + // Message item example + _ExampleCard( + title: 'Message List Item', + description: 'Medium avatar in conversation list', + child: Row( + children: [ + StreamAvatar( + size: StreamAvatarSize.md, + backgroundColor: palette[0].backgroundColor, + foregroundColor: palette[0].foregroundColor, + placeholder: (context) => const Text('JD'), + ), + SizedBox(width: spacing.sm), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'John Doe', + style: textTheme.bodyEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + Text( + 'Hey! Are you free for a call?', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + ], + ), + ], + ), + ), + SizedBox(height: spacing.sm), + + // Compact list example + _ExampleCard( + title: 'Compact List', + description: 'Small avatars for dense layouts', + child: Column( children: [ - for (var i = 0; i < palette.length; i++) - Column( - mainAxisSize: MainAxisSize.min, + for (var i = 0; i < 3; i++) ...[ + Row( children: [ StreamAvatar( - size: StreamAvatarSize.lg, - backgroundColor: palette[i].backgroundColor, - foregroundColor: palette[i].foregroundColor, + size: StreamAvatarSize.sm, + backgroundColor: palette[i % palette.length].backgroundColor, + foregroundColor: palette[i % palette.length].foregroundColor, placeholder: (context) => Text(_getInitials(i)), ), - const SizedBox(height: 8), + SizedBox(width: spacing.sm + spacing.xxs), Text( - 'Palette ${i + 1}', - style: textTheme.metadataDefault.copyWith( - color: colorScheme.textSecondary, + ['Alice Brown', 'Bob Smith', 'Carol White'][i], + style: textTheme.captionDefault.copyWith( + color: colorScheme.textPrimary, ), ), ], ), + if (i < 2) SizedBox(height: spacing.sm), + ], ], ), - ], - ), - ), - ); + ), + ], + ); + } } -// ============================================================================= -// Real-world Example -// ============================================================================= +class _ExampleCard extends StatelessWidget { + const _ExampleCard({ + required this.title, + required this.description, + required this.child, + }); -@widgetbook.UseCase( - name: 'Real-world Example', - type: StreamAvatar, - path: '[Components]/Avatar', -) -Widget buildStreamAvatarExample(BuildContext context) { - final theme = StreamTheme.of(context); - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; - final palette = colorScheme.avatarPalette; + final String title; + final String description; + final Widget child; - return Center( - child: Container( - padding: const EdgeInsets.all(24), + @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, + clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.lg), + boxShadow: boxShadow.elevation1, + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), + border: Border.all(color: colorScheme.borderSurfaceSubtle), ), child: Column( - mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Common Patterns', - style: textTheme.headingSm.copyWith( - color: colorScheme.textPrimary, - ), - ), - const SizedBox(height: 16), - // User profile header - Container( - padding: const EdgeInsets.all(16), - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), - ), - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: colorScheme.borderSurfaceSubtle), - ), - child: Row( - mainAxisSize: MainAxisSize.min, + // Header + Padding( + padding: EdgeInsets.fromLTRB(spacing.md, spacing.sm, spacing.md, spacing.sm), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - StreamAvatar( - imageUrl: _sampleImageUrl, - size: StreamAvatarSize.lg, - placeholder: (context) => const Text('JD'), + Text( + title, + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Jane Doe', - style: textTheme.headingSm.copyWith( - color: colorScheme.textPrimary, - ), - ), - Text( - 'Product Designer', - style: textTheme.bodyDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - ], + Text( + description, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + ), ), ], ), ), - const SizedBox(height: 12), - // Message list item + Divider( + height: 1, + color: colorScheme.borderSurfaceSubtle, + ), + // Content Container( - padding: const EdgeInsets.all(12), - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), - ), - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: colorScheme.borderSurfaceSubtle), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - StreamAvatar( - size: StreamAvatarSize.md, - backgroundColor: palette[0].backgroundColor, - foregroundColor: palette[0].foregroundColor, - placeholder: (context) => const Text('JD'), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'John Doe', - style: textTheme.bodyEmphasis.copyWith( - color: colorScheme.textPrimary, - ), - ), - Text( - 'Hey! Are you free for a call?', - style: textTheme.captionDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - ], - ), - ], - ), + padding: EdgeInsets.all(spacing.md), + color: colorScheme.backgroundSurface, + child: child, ), ], ), - ), - ); + ); + } +} + +// ============================================================================= +// Shared Widgets +// ============================================================================= + +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, + ), + ), + ); + } } String _getInitials(int index) { diff --git a/apps/design_system_gallery/lib/components/stream_avatar_stack.dart b/apps/design_system_gallery/lib/components/stream_avatar_stack.dart index debf6b8..4a3612c 100644 --- a/apps/design_system_gallery/lib/components/stream_avatar_stack.dart +++ b/apps/design_system_gallery/lib/components/stream_avatar_stack.dart @@ -58,7 +58,7 @@ Widget buildStreamAvatarStackPlayground(BuildContext context) { description: 'Use images or show initials placeholder.', ); - final colorScheme = StreamTheme.of(context).colorScheme; + final colorScheme = context.streamColorScheme; final palette = colorScheme.avatarPalette; return Center( @@ -80,142 +80,456 @@ Widget buildStreamAvatarStackPlayground(BuildContext context) { } // ============================================================================= -// Real-world Example +// Showcase // ============================================================================= @widgetbook.UseCase( - name: 'Real-world Example', + name: 'Showcase', type: StreamAvatarStack, path: '[Components]/Avatar', ) -Widget buildStreamAvatarStackExample(BuildContext context) { - final theme = StreamTheme.of(context); - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; - final palette = colorScheme.avatarPalette; +Widget buildStreamAvatarStackShowcase(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; - return Center( - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), - ), + return DefaultTextStyle( + style: textTheme.bodyDefault.copyWith(color: colorScheme.textPrimary), + child: SingleChildScrollView( + padding: EdgeInsets.all(spacing.lg), child: Column( - mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Common Patterns', - style: textTheme.headingSm.copyWith( - color: colorScheme.textPrimary, - ), + // Configuration options + const _ConfigurationSection(), + SizedBox(height: spacing.xl), + + // Usage patterns + const _UsagePatternsSection(), + ], + ), + ), + ); +} + +// ============================================================================= +// Configuration Section +// ============================================================================= + +class _ConfigurationSection extends StatelessWidget { + const _ConfigurationSection(); + + @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; + final palette = colorScheme.avatarPalette; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'CONFIGURATION'), + 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, ), - const SizedBox(height: 16), - // Group chat - Container( - padding: const EdgeInsets.all(12), - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), - ), - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: colorScheme.borderSurfaceSubtle), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - StreamAvatarStack( - size: StreamAvatarSize.sm, - children: [ - for (var i = 0; i < 3; i++) - StreamAvatar( - imageUrl: _sampleImages[i], - backgroundColor: palette[i % palette.length].backgroundColor, - foregroundColor: palette[i % palette.length].foregroundColor, - placeholder: (context) => Text(_getInitials(i)), - ), - ], + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), + border: Border.all(color: colorScheme.borderSurfaceSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Overlap demonstration + Text( + 'Overlap', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + SizedBox(height: spacing.sm), + Text( + 'Controls how much avatars overlap each other', + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'John, Sarah, Mike', - style: textTheme.bodyEmphasis.copyWith( - color: colorScheme.textPrimary, - ), + ), + SizedBox(height: spacing.sm), + Row( + children: [ + _OverlapDemo( + label: '0.0', + overlap: 0, + palette: palette, + ), + SizedBox(width: spacing.lg), + _OverlapDemo( + label: '0.3', + overlap: 0.3, + palette: palette, + ), + SizedBox(width: spacing.lg), + _OverlapDemo( + label: '0.5', + overlap: 0.5, + palette: palette, + ), + ], + ), + SizedBox(height: spacing.md), + Divider(color: colorScheme.borderSurfaceSubtle), + SizedBox(height: spacing.md), + // Max avatars demonstration + Text( + 'Max Visible', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + SizedBox(height: spacing.sm), + Text( + 'Shows "+N" indicator when avatars exceed max', + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + ), + ), + SizedBox(height: spacing.sm), + Row( + children: [ + _MaxDemo( + label: 'max: 3', + max: 3, + count: 6, + palette: palette, + ), + SizedBox(width: spacing.lg), + _MaxDemo( + label: 'max: 5', + max: 5, + count: 8, + palette: palette, + ), + ], + ), + ], + ), + ), + ], + ); + } +} + +class _OverlapDemo extends StatelessWidget { + const _OverlapDemo({ + required this.label, + required this.overlap, + required this.palette, + }); + + final String label; + final double overlap; + final List palette; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Column( + children: [ + StreamAvatarStack( + size: StreamAvatarSize.sm, + overlap: overlap, + children: [ + for (var i = 0; i < 3; i++) + StreamAvatar( + backgroundColor: palette[i % palette.length].backgroundColor, + foregroundColor: palette[i % palette.length].foregroundColor, + placeholder: (context) => Text(_getInitials(i)), + ), + ], + ), + SizedBox(height: spacing.xs + spacing.xxs), + Text( + label, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + fontFamily: 'monospace', + ), + ), + ], + ); + } +} + +class _MaxDemo extends StatelessWidget { + const _MaxDemo({ + required this.label, + required this.max, + required this.count, + required this.palette, + }); + + final String label; + final int max; + final int count; + final List palette; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Column( + children: [ + StreamAvatarStack( + size: StreamAvatarSize.sm, + max: max, + children: [ + for (var i = 0; i < count; i++) + StreamAvatar( + imageUrl: _sampleImages[i % _sampleImages.length], + backgroundColor: palette[i % palette.length].backgroundColor, + foregroundColor: palette[i % palette.length].foregroundColor, + placeholder: (context) => Text(_getInitials(i)), + ), + ], + ), + SizedBox(height: spacing.xs + spacing.xxs), + Text( + label, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + fontFamily: 'monospace', + ), + ), + ], + ); + } +} + +// ============================================================================= +// Usage Patterns Section +// ============================================================================= + +class _UsagePatternsSection extends StatelessWidget { + const _UsagePatternsSection(); + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + final palette = colorScheme.avatarPalette; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'USAGE PATTERNS'), + SizedBox(height: spacing.md), + + // Group chat example + _ExampleCard( + title: 'Group Chat', + description: 'Show participants in a conversation', + child: Row( + children: [ + StreamAvatarStack( + size: StreamAvatarSize.sm, + children: [ + for (var i = 0; i < 3; i++) + StreamAvatar( + imageUrl: _sampleImages[i], + backgroundColor: palette[i % palette.length].backgroundColor, + foregroundColor: palette[i % palette.length].foregroundColor, + placeholder: (context) => Text(_getInitials(i)), ), - Text( - 'Active now', - style: textTheme.captionDefault.copyWith( - color: colorScheme.accentSuccess, - ), + ], + ), + SizedBox(width: spacing.sm), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'John, Sarah, Mike', + style: textTheme.bodyEmphasis.copyWith( + color: colorScheme.textPrimary, ), - ], - ), - ], - ), + ), + Text( + 'Active now', + style: textTheme.captionDefault.copyWith( + color: colorScheme.accentSuccess, + ), + ), + ], + ), + ], ), - const SizedBox(height: 12), - // Team with overflow - Container( - padding: const EdgeInsets.all(12), - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), - ), - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: colorScheme.borderSurfaceSubtle), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - StreamAvatarStack( - size: StreamAvatarSize.sm, - max: 4, - children: [ - for (var i = 0; i < 8; i++) - StreamAvatar( - imageUrl: _sampleImages[i % _sampleImages.length], - backgroundColor: palette[i % palette.length].backgroundColor, - foregroundColor: palette[i % palette.length].foregroundColor, - placeholder: (context) => Text(_getInitials(i)), - ), - ], - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Design Team', - style: textTheme.bodyEmphasis.copyWith( - color: colorScheme.textPrimary, - ), + ), + SizedBox(height: spacing.sm), + + // Team with overflow example + _ExampleCard( + title: 'Team Members', + description: 'Show team with overflow indicator', + child: Row( + children: [ + StreamAvatarStack( + size: StreamAvatarSize.sm, + max: 4, + children: [ + for (var i = 0; i < 8; i++) + StreamAvatar( + imageUrl: _sampleImages[i % _sampleImages.length], + backgroundColor: palette[i % palette.length].backgroundColor, + foregroundColor: palette[i % palette.length].foregroundColor, + placeholder: (context) => Text(_getInitials(i)), ), - Text( - '8 members', - style: textTheme.captionDefault.copyWith( - color: colorScheme.textSecondary, - ), + ], + ), + SizedBox(width: spacing.sm), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Design Team', + style: textTheme.bodyEmphasis.copyWith( + color: colorScheme.textPrimary, ), - ], + ), + Text( + '8 members', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + ], + ), + ], + ), + ), + ], + ); + } +} + +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, + clipBehavior: Clip.antiAlias, + 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.borderSurfaceSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + 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.borderSurfaceSubtle, + ), + // Content + Container( + padding: EdgeInsets.all(spacing.md), + color: colorScheme.backgroundSurface, + child: child, + ), ], ), - ), - ); + ); + } +} + +// ============================================================================= +// Shared Widgets +// ============================================================================= + +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, + ), + ), + ); + } } String _getInitials(int index) { diff --git a/apps/design_system_gallery/lib/components/stream_online_indicator.dart b/apps/design_system_gallery/lib/components/stream_online_indicator.dart index eeb20ce..b5f2c35 100644 --- a/apps/design_system_gallery/lib/components/stream_online_indicator.dart +++ b/apps/design_system_gallery/lib/components/stream_online_indicator.dart @@ -36,118 +36,164 @@ Widget buildStreamOnlineIndicatorPlayground(BuildContext context) { } // ============================================================================= -// Size Variants +// Showcase // ============================================================================= @widgetbook.UseCase( - name: 'Size Variants', + name: 'Showcase', type: StreamOnlineIndicator, path: '[Components]/Indicator', ) -Widget buildStreamOnlineIndicatorSizes(BuildContext context) { - final streamTheme = StreamTheme.of(context); - final textTheme = streamTheme.textTheme; - final colorScheme = streamTheme.colorScheme; +Widget buildStreamOnlineIndicatorShowcase(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + return DefaultTextStyle( + style: textTheme.bodyDefault.copyWith(color: colorScheme.textPrimary), + child: SingleChildScrollView( + padding: EdgeInsets.all(spacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Size variants + const _SizeVariantsSection(), + SizedBox(height: spacing.xl), + + // Usage patterns + const _UsagePatternsSection(), + ], + ), + ), + ); +} + +// ============================================================================= +// Size Variants Section +// ============================================================================= + +class _SizeVariantsSection extends StatelessWidget { + const _SizeVariantsSection(); + + @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: [ - _SizeVariant( - label: 'Small (8px)', - size: StreamOnlineIndicatorSize.sm, - isOnline: true, - textTheme: textTheme, - colorScheme: colorScheme, - ), - const SizedBox(height: 24), - _SizeVariant( - label: 'Medium (12px)', - size: StreamOnlineIndicatorSize.md, - isOnline: true, - textTheme: textTheme, - colorScheme: colorScheme, - ), - const SizedBox(height: 24), - _SizeVariant( - label: 'Large (14px)', - size: StreamOnlineIndicatorSize.lg, - isOnline: true, - textTheme: textTheme, - colorScheme: colorScheme, - ), - const SizedBox(height: 32), - Divider(color: colorScheme.borderSurfaceSubtle), - const SizedBox(height: 32), - _SizeVariant( - label: 'Small (8px)', - size: StreamOnlineIndicatorSize.sm, - isOnline: false, - textTheme: textTheme, - colorScheme: colorScheme, - ), - const SizedBox(height: 24), - _SizeVariant( - label: 'Medium (12px)', - size: StreamOnlineIndicatorSize.md, - isOnline: false, - textTheme: textTheme, - colorScheme: colorScheme, - ), - const SizedBox(height: 24), - _SizeVariant( - label: 'Large (14px)', - size: StreamOnlineIndicatorSize.lg, - isOnline: false, - textTheme: textTheme, - colorScheme: colorScheme, + const _SectionLabel(label: 'SIZE VARIANTS'), + 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.borderSurfaceSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Online states + Text( + 'Online', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + SizedBox(height: spacing.sm), + Row( + children: [ + for (final size in StreamOnlineIndicatorSize.values) ...[ + _SizeDemo(size: size, isOnline: true), + if (size != StreamOnlineIndicatorSize.values.last) SizedBox(width: spacing.xl), + ], + ], + ), + SizedBox(height: spacing.md), + Divider(color: colorScheme.borderSurfaceSubtle), + SizedBox(height: spacing.md), + // Offline states + Text( + 'Offline', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + SizedBox(height: spacing.sm), + Row( + children: [ + for (final size in StreamOnlineIndicatorSize.values) ...[ + _SizeDemo(size: size, isOnline: false), + if (size != StreamOnlineIndicatorSize.values.last) SizedBox(width: spacing.xl), + ], + ], + ), + ], + ), ), ], - ), - ); + ); + } } -class _SizeVariant extends StatelessWidget { - const _SizeVariant({ - required this.label, - required this.size, - required this.isOnline, - required this.textTheme, - required this.colorScheme, - }); +class _SizeDemo extends StatelessWidget { + const _SizeDemo({required this.size, required this.isOnline}); - final String label; final StreamOnlineIndicatorSize size; final bool isOnline; - final StreamTextTheme textTheme; - final StreamColorScheme colorScheme; + + String _getPixelSize(StreamOnlineIndicatorSize size) { + return switch (size) { + StreamOnlineIndicatorSize.sm => '8px', + StreamOnlineIndicatorSize.md => '12px', + StreamOnlineIndicatorSize.lg => '14px', + }; + } @override Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Column( children: [ - StreamOnlineIndicator( - isOnline: isOnline, - size: size, - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: textTheme.captionEmphasis.copyWith( - color: colorScheme.textPrimary, - ), - ), - Text( - isOnline ? 'Online' : 'Offline', - style: textTheme.metadataDefault.copyWith( - color: colorScheme.textSecondary, - ), + SizedBox( + width: 32, + height: 32, + child: Center( + child: StreamOnlineIndicator( + isOnline: isOnline, + size: size, ), - ], + ), + ), + SizedBox(height: spacing.sm), + Text( + size.name.toUpperCase(), + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + Text( + _getPixelSize(size), + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + fontFamily: 'monospace', + fontSize: 10, + ), ), ], ); @@ -155,91 +201,75 @@ class _SizeVariant extends StatelessWidget { } // ============================================================================= -// Real-world Example +// Usage Patterns Section // ============================================================================= -@widgetbook.UseCase( - name: 'Real-world Example', - type: StreamOnlineIndicator, - path: '[Components]/Indicator', -) -Widget buildStreamOnlineIndicatorExample(BuildContext context) { - final streamTheme = StreamTheme.of(context); - final textTheme = streamTheme.textTheme; - final colorScheme = streamTheme.colorScheme; +class _UsagePatternsSection extends StatelessWidget { + const _UsagePatternsSection(); - return Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Online Status Indicators', - style: textTheme.headingSm.copyWith( - color: colorScheme.textPrimary, - ), - ), - const SizedBox(height: 8), - Text( - 'Indicators positioned on avatars to show presence', - style: textTheme.captionDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - const SizedBox(height: 32), - Wrap( + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'USAGE PATTERNS'), + SizedBox(height: spacing.md), + + // Avatar with indicator + const _ExampleCard( + title: 'Avatar with Status', + description: 'Indicator positioned on avatar corner', + child: Row( spacing: 24, - runSpacing: 24, - alignment: WrapAlignment.center, children: [ _AvatarWithIndicator( name: 'Sarah Chen', isOnline: true, - textTheme: textTheme, - colorScheme: colorScheme, ), _AvatarWithIndicator( name: 'Alex Kim', isOnline: true, - textTheme: textTheme, - colorScheme: colorScheme, ), _AvatarWithIndicator( name: 'Jordan Lee', isOnline: false, - textTheme: textTheme, - colorScheme: colorScheme, ), _AvatarWithIndicator( name: 'Taylor Park', isOnline: true, - textTheme: textTheme, - colorScheme: colorScheme, ), ], ), - ], - ), - ), - ); + ), + SizedBox(height: spacing.sm), + + // Inline status + const _ExampleCard( + title: 'Inline Status', + description: 'Indicator next to user name', + child: _InlineStatusGroup(), + ), + ], + ); + } } class _AvatarWithIndicator extends StatelessWidget { const _AvatarWithIndicator({ required this.name, required this.isOnline, - required this.textTheme, - required this.colorScheme, }); final String name; final bool isOnline; - final StreamTextTheme textTheme; - final StreamColorScheme colorScheme; @override Widget build(BuildContext context) { + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; final initials = name.split(' ').map((n) => n[0]).join(); return Column( @@ -249,12 +279,7 @@ class _AvatarWithIndicator extends StatelessWidget { children: [ StreamAvatar( size: StreamAvatarSize.lg, - placeholder: (context) => Text( - initials, - style: textTheme.captionEmphasis.copyWith( - color: colorScheme.textOnAccent, - ), - ), + placeholder: (context) => Text(initials), ), Positioned( right: 0, @@ -266,7 +291,7 @@ class _AvatarWithIndicator extends StatelessWidget { ), ], ), - const SizedBox(height: 8), + SizedBox(height: spacing.sm), Text( name, style: textTheme.captionDefault.copyWith( @@ -283,3 +308,160 @@ class _AvatarWithIndicator extends StatelessWidget { ); } } + +class _InlineStatusGroup extends StatelessWidget { + const _InlineStatusGroup(); + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const _InlineStatus(name: 'Online User', isOnline: true), + SizedBox(height: spacing.sm), + const _InlineStatus(name: 'Away User', isOnline: false), + ], + ); + } +} + +class _InlineStatus extends StatelessWidget { + const _InlineStatus({ + required this.name, + required this.isOnline, + }); + + final String name; + final bool isOnline; + + @override + Widget build(BuildContext context) { + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + StreamOnlineIndicator( + isOnline: isOnline, + size: StreamOnlineIndicatorSize.sm, + ), + SizedBox(width: spacing.sm), + Text( + name, + style: textTheme.captionDefault.copyWith( + color: colorScheme.textPrimary, + ), + ), + ], + ); + } +} + +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, + clipBehavior: Clip.antiAlias, + 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.borderSurfaceSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + 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.borderSurfaceSubtle, + ), + // Content + Container( + padding: EdgeInsets.all(spacing.md), + color: colorScheme.backgroundSurface, + child: child, + ), + ], + ), + ); + } +} + +// ============================================================================= +// Shared Widgets +// ============================================================================= + +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/apps/design_system_gallery/lib/config/theme_configuration.dart b/apps/design_system_gallery/lib/config/theme_configuration.dart index 89729be..ddcec78 100644 --- a/apps/design_system_gallery/lib/config/theme_configuration.dart +++ b/apps/design_system_gallery/lib/config/theme_configuration.dart @@ -389,183 +389,176 @@ class ThemeConfiguration extends ChangeNotifier { /// Builds a Material ThemeData that uses Stream colors. /// Use this for applying Stream theming to regular Flutter widgets. ThemeData buildMaterialTheme() { - final cs = _themeData.colorScheme; - final isDark = _brightness == Brightness.dark; + final ts = themeData.textTheme; + final radius = themeData.radius; + final isDark = brightness == Brightness.dark; + + // Common radius values (StreamRadius returns Radius, use BorderRadius.all) + final componentRadius = BorderRadius.all(radius.md); + final dialogRadius = BorderRadius.all(radius.lg); + final smallRadius = BorderRadius.all(radius.sm); + + // Shared ColorScheme properties - uses class getters for colors + final materialColorScheme = (isDark ? ColorScheme.dark : ColorScheme.light)( + primary: accentPrimary, + secondary: accentPrimary, + tertiary: accentNeutral, + error: accentError, + surface: backgroundSurface, + surfaceContainerHighest: backgroundSurfaceSubtle, + onPrimary: textOnAccent, + onSecondary: textOnAccent, + onSurface: textPrimary, + onSurfaceVariant: textSecondary, + onError: textOnAccent, + outline: borderSurface, + outlineVariant: borderSurfaceSubtle, + ); return ThemeData( - brightness: _brightness, + brightness: brightness, useMaterial3: true, + colorScheme: materialColorScheme, + // StreamTheme extension - enables StreamTheme.of(context) and context extensions + extensions: [themeData], // Colors - primaryColor: cs.accentPrimary, - scaffoldBackgroundColor: cs.backgroundApp, - cardColor: cs.backgroundSurface, - dividerColor: cs.borderSurfaceSubtle, - disabledColor: cs.textDisabled, - hintColor: cs.textTertiary, - // Color Scheme - colorScheme: isDark - ? ColorScheme.dark( - primary: cs.accentPrimary, - secondary: cs.accentPrimary, - tertiary: cs.accentNeutral, - error: cs.accentError, - surface: cs.backgroundSurface, - surfaceContainerHighest: cs.backgroundSurfaceSubtle, - onPrimary: cs.textOnAccent, - onSecondary: cs.textOnAccent, - onSurface: cs.textPrimary, - onSurfaceVariant: cs.textSecondary, - onError: cs.textOnAccent, - outline: cs.borderSurface, - outlineVariant: cs.borderSurfaceSubtle, - ) - : ColorScheme.light( - primary: cs.accentPrimary, - secondary: cs.accentPrimary, - tertiary: cs.accentNeutral, - error: cs.accentError, - surface: cs.backgroundSurface, - surfaceContainerHighest: cs.backgroundSurfaceSubtle, - onPrimary: cs.textOnAccent, - onSecondary: cs.textOnAccent, - onSurface: cs.textPrimary, - onSurfaceVariant: cs.textSecondary, - onError: cs.textOnAccent, - outline: cs.borderSurface, - outlineVariant: cs.borderSurfaceSubtle, - ), + primaryColor: accentPrimary, + scaffoldBackgroundColor: backgroundApp, + cardColor: backgroundSurface, + dividerColor: borderSurfaceSubtle, + disabledColor: textDisabled, + hintColor: textTertiary, // Dialog dialogTheme: DialogThemeData( - backgroundColor: cs.backgroundSurface, + backgroundColor: backgroundSurface, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: BorderSide(color: cs.borderSurfaceSubtle), + borderRadius: dialogRadius, + side: BorderSide(color: borderSurfaceSubtle), ), - titleTextStyle: TextStyle( - color: cs.textPrimary, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - contentTextStyle: TextStyle(color: cs.textSecondary, fontSize: 14), + titleTextStyle: ts.bodyEmphasis.copyWith(color: textPrimary), + contentTextStyle: ts.bodyDefault.copyWith(color: textSecondary), ), // AppBar appBarTheme: AppBarTheme( - backgroundColor: cs.backgroundSurface, - foregroundColor: cs.textPrimary, + backgroundColor: backgroundSurface, + foregroundColor: textPrimary, surfaceTintColor: Colors.transparent, elevation: 0, ), // Buttons filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( - backgroundColor: cs.accentPrimary, - foregroundColor: cs.textOnAccent, - disabledBackgroundColor: cs.stateDisabled, - disabledForegroundColor: cs.textDisabled, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + backgroundColor: accentPrimary, + foregroundColor: textOnAccent, + disabledBackgroundColor: stateDisabled, + disabledForegroundColor: textDisabled, + shape: RoundedRectangleBorder(borderRadius: componentRadius), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( - foregroundColor: cs.textPrimary, - side: BorderSide(color: cs.borderSurface), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + foregroundColor: textPrimary, + side: BorderSide(color: borderSurface), + shape: RoundedRectangleBorder(borderRadius: componentRadius), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( - foregroundColor: cs.accentPrimary, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + foregroundColor: accentPrimary, + shape: RoundedRectangleBorder(borderRadius: componentRadius), ), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - backgroundColor: cs.backgroundSurface, - foregroundColor: cs.textPrimary, + backgroundColor: backgroundSurface, + foregroundColor: textPrimary, surfaceTintColor: Colors.transparent, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shape: RoundedRectangleBorder(borderRadius: componentRadius), ), ), // Input inputDecorationTheme: InputDecorationTheme( - fillColor: cs.backgroundApp, + fillColor: backgroundApp, filled: true, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: cs.borderSurface), + borderRadius: componentRadius, + borderSide: BorderSide(color: borderSurface), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: cs.borderSurfaceSubtle), + borderRadius: componentRadius, + borderSide: BorderSide(color: borderSurfaceSubtle), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: cs.accentPrimary, width: 2), + borderRadius: componentRadius, + borderSide: BorderSide(color: accentPrimary, width: 2), ), errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: cs.accentError), + borderRadius: componentRadius, + borderSide: BorderSide(color: accentError), ), - hintStyle: TextStyle(color: cs.textTertiary), - labelStyle: TextStyle(color: cs.textSecondary), + hintStyle: ts.bodyDefault.copyWith(color: textTertiary), + labelStyle: ts.bodyDefault.copyWith(color: textSecondary), ), // Dropdown dropdownMenuTheme: DropdownMenuThemeData( menuStyle: MenuStyle( - backgroundColor: WidgetStatePropertyAll(cs.backgroundSurface), + backgroundColor: WidgetStatePropertyAll(backgroundSurface), surfaceTintColor: const WidgetStatePropertyAll(Colors.transparent), shape: WidgetStatePropertyAll( RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide(color: cs.borderSurfaceSubtle), + borderRadius: componentRadius, + side: BorderSide(color: borderSurfaceSubtle), ), ), ), ), // PopupMenu popupMenuTheme: PopupMenuThemeData( - color: cs.backgroundSurface, + color: backgroundSurface, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide(color: cs.borderSurfaceSubtle), + borderRadius: componentRadius, + side: BorderSide(color: borderSurfaceSubtle), ), - textStyle: TextStyle(color: cs.textPrimary), + textStyle: ts.bodyDefault.copyWith(color: textPrimary), ), // Scrollbar scrollbarTheme: ScrollbarThemeData( - thumbColor: WidgetStatePropertyAll(cs.systemScrollbar), - trackColor: WidgetStatePropertyAll(cs.backgroundSurfaceSubtle), - radius: const Radius.circular(4), + thumbColor: WidgetStatePropertyAll(systemScrollbar), + trackColor: WidgetStatePropertyAll(backgroundSurfaceSubtle), + radius: radius.max, thickness: const WidgetStatePropertyAll(6), ), // Tooltip tooltipTheme: TooltipThemeData( decoration: BoxDecoration( - color: cs.backgroundSurfaceStrong, - borderRadius: BorderRadius.circular(6), + color: backgroundSurfaceStrong, + borderRadius: smallRadius, ), - textStyle: TextStyle(color: cs.textPrimary, fontSize: 12), + textStyle: ts.captionDefault.copyWith(color: textPrimary), ), // Divider dividerTheme: DividerThemeData( - color: cs.borderSurfaceSubtle, + color: borderSurfaceSubtle, thickness: 1, ), // Icon - iconTheme: IconThemeData(color: cs.textSecondary), - // Text + iconTheme: IconThemeData(color: textSecondary), + // Text - mapped to closest Stream styles by size/weight textTheme: TextTheme( - bodyLarge: TextStyle(color: cs.textPrimary), - bodyMedium: TextStyle(color: cs.textPrimary), - bodySmall: TextStyle(color: cs.textSecondary), - labelLarge: TextStyle(color: cs.textPrimary), - labelMedium: TextStyle(color: cs.textSecondary), - labelSmall: TextStyle(color: cs.textTertiary), - titleLarge: TextStyle(color: cs.textPrimary, fontWeight: FontWeight.w600), - titleMedium: TextStyle(color: cs.textPrimary, fontWeight: FontWeight.w600), - titleSmall: TextStyle(color: cs.textPrimary, fontWeight: FontWeight.w500), + // Titles - for app bar, dialogs, navigation + titleLarge: ts.headingLg.copyWith(color: textPrimary), + titleMedium: ts.bodyEmphasis.copyWith(color: textPrimary), + titleSmall: ts.captionEmphasis.copyWith(color: textPrimary), + // Body - main content text + bodyLarge: ts.bodyEmphasis.copyWith(color: textPrimary), + bodyMedium: ts.bodyDefault.copyWith(color: textPrimary), + bodySmall: ts.captionDefault.copyWith(color: textSecondary), + // Labels - buttons, chips, navigation items + labelLarge: ts.captionEmphasis.copyWith(color: textPrimary), + labelMedium: ts.metadataEmphasis.copyWith(color: textSecondary), + labelSmall: ts.metadataDefault.copyWith(color: textTertiary), ), ); } diff --git a/apps/design_system_gallery/lib/core/preview_wrapper.dart b/apps/design_system_gallery/lib/core/preview_wrapper.dart index 600bd05..bff9b67 100644 --- a/apps/design_system_gallery/lib/core/preview_wrapper.dart +++ b/apps/design_system_gallery/lib/core/preview_wrapper.dart @@ -1,14 +1,14 @@ import 'package:device_frame_plus/device_frame_plus.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; import '../config/preview_configuration.dart'; -import '../config/theme_configuration.dart'; /// Wrapper widget that applies device frame and text scale to the preview. /// -/// Uses [ListenableBuilder] to explicitly react to [ThemeConfiguration] -/// and [PreviewConfiguration] changes. +/// Uses [ListenableBuilder] to explicitly react to [PreviewConfiguration] changes. +/// StreamTheme is already available via MaterialApp's theme extensions. class PreviewWrapper extends StatelessWidget { const PreviewWrapper({super.key, required this.child}); @@ -16,70 +16,51 @@ class PreviewWrapper extends StatelessWidget { @override Widget build(BuildContext context) { - // Use ListenableBuilder to explicitly listen to both configurations - return ListenableBuilder( - listenable: Listenable.merge([ - context.read(), - context.read(), - ]), - builder: (context, _) { - final themeConfig = context.read(); - final previewConfig = context.read(); + final previewConfig = context.watch(); - final streamTheme = themeConfig.themeData; - final colorScheme = streamTheme.colorScheme; - final boxShadow = streamTheme.boxShadow; + final colorScheme = context.streamColorScheme; + final boxShadow = context.streamBoxShadow; + final radius = context.streamRadius; + final spacing = context.streamSpacing; - // Provide StreamTheme via Material theme extension - this is how - // StreamTheme.of(context) finds the theme in the widget tree - final content = Theme( - data: ThemeData( - brightness: themeConfig.brightness, - useMaterial3: true, - scaffoldBackgroundColor: colorScheme.backgroundApp, - extensions: [streamTheme], - ), - child: MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear(previewConfig.textScale), - ), - child: ColoredBox( - color: colorScheme.backgroundApp, - child: child, - ), - ), - ); - - if (!previewConfig.showDeviceFrame) { - return Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 540, maxHeight: 900), - margin: const EdgeInsets.all(24), - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(20), - boxShadow: boxShadow.elevation3, - ), - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - border: Border.all(color: colorScheme.borderSurfaceSubtle), - ), - child: content, - ), - ); - } + final content = MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear(previewConfig.textScale), + ), + child: ColoredBox( + color: colorScheme.backgroundApp, + child: child, + ), + ); - return Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: DeviceFrame( - device: previewConfig.selectedDevice, - screen: content, - ), + if (previewConfig.showDeviceFrame) { + return Center( + child: Padding( + padding: EdgeInsets.all(spacing.xl), + child: DeviceFrame( + device: previewConfig.selectedDevice, + screen: content, ), - ); - }, + ), + ); + } + + return Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 540, maxHeight: 900), + margin: EdgeInsets.all(spacing.lg), + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: colorScheme.backgroundApp, + borderRadius: BorderRadius.all(radius.xl), + boxShadow: boxShadow.elevation3, + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.xl), + border: Border.all(color: colorScheme.borderSurfaceSubtle), + ), + child: content, + ), ); } } diff --git a/apps/design_system_gallery/lib/core/stream_icons.dart b/apps/design_system_gallery/lib/core/stream_icons.dart new file mode 100644 index 0000000..0e87e09 --- /dev/null +++ b/apps/design_system_gallery/lib/core/stream_icons.dart @@ -0,0 +1,10 @@ +import 'package:svg_icon_widget/svg_icon_widget.dart'; + +/// Stream icon assets used throughout the gallery. +abstract final class StreamIcons { + /// The Stream boat logo. + static const logo = SvgIconData( + 'assets/stream_logo.svg', + preserveColors: true, + ); +} diff --git a/apps/design_system_gallery/lib/primitives/colors.dart b/apps/design_system_gallery/lib/primitives/colors.dart new file mode 100644 index 0000000..3458394 --- /dev/null +++ b/apps/design_system_gallery/lib/primitives/colors.dart @@ -0,0 +1,533 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart'; + +@UseCase( + name: 'All Colors', + type: StreamColors, + path: '[App Foundation]/Primitives/Colors', +) +Widget buildStreamColorsShowcase(BuildContext context) { + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + 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: [ + // Color swatches - full width + const _ColorSwatchesList(), + SizedBox(height: spacing.xl), + + // Neutrals section + const _NeutralsSection(), + ], + ), + ), + ); +} + +/// Full-width color swatches +class _ColorSwatchesList extends StatelessWidget { + const _ColorSwatchesList(); + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + const swatches = [ + _SwatchData( + name: 'blue', + swatch: StreamColors.blue, + usage: 'Primary actions, links, focus states', + ), + _SwatchData( + name: 'cyan', + swatch: StreamColors.cyan, + usage: 'Info states, secondary highlights', + ), + _SwatchData( + name: 'green', + swatch: StreamColors.green, + usage: 'Success states, positive feedback', + ), + _SwatchData( + name: 'purple', + swatch: StreamColors.purple, + usage: 'Premium features, special content', + ), + _SwatchData( + name: 'yellow', + swatch: StreamColors.yellow, + usage: 'Warnings, attention states', + ), + _SwatchData( + name: 'red', + swatch: StreamColors.red, + usage: 'Errors, destructive actions', + ), + _SwatchData( + name: 'slate', + swatch: StreamColors.slate, + usage: 'Dark backgrounds, text on light', + ), + _SwatchData( + name: 'neutral', + swatch: StreamColors.neutral, + usage: 'Light backgrounds, borders', + ), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'COLOR SWATCHES'), + SizedBox(height: spacing.md), + ...swatches.map((data) => _FullWidthSwatchCard(data: data)), + ], + ); + } +} + +class _FullWidthSwatchCard extends StatelessWidget { + const _FullWidthSwatchCard({required this.data}); + + final _SwatchData data; + + @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; + final shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]; + + return Padding( + padding: EdgeInsets.only(bottom: spacing.md), + child: Container( + clipBehavior: Clip.antiAlias, + 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.borderSurfaceSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Padding( + padding: EdgeInsets.all(spacing.sm), + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: data.swatch.shade500, + borderRadius: BorderRadius.all(radius.sm), + ), + ), + SizedBox(width: spacing.sm + spacing.xxs), + Text( + 'StreamColors.${data.name}', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + fontFamily: 'monospace', + ), + ), + SizedBox(width: spacing.sm), + Text( + '— ${data.usage}', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textTertiary, + ), + ), + ], + ), + ), + // Full width shade strip + Row( + children: shades.map((shade) { + final color = data.swatch[shade]!; + final textColor = _getTextColorForShade(data.swatch, shade); + + return Expanded( + child: InkWell( + onTap: () { + Clipboard.setData( + ClipboardData( + text: 'StreamColors.${data.name}.shade$shade', + ), + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Copied: StreamColors.${data.name}.shade$shade', + ), + duration: const Duration(seconds: 1), + behavior: SnackBarBehavior.floating, + ), + ); + }, + child: Container( + height: 56, + color: color, + child: Center( + child: Text( + '$shade', + style: textTheme.metadataEmphasis.copyWith( + color: textColor, + fontSize: 10, + ), + ), + ), + ), + ), + ); + }).toList(), + ), + ], + ), + ), + ); + } +} + +/// Neutrals section +class _NeutralsSection extends StatelessWidget { + const _NeutralsSection(); + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'NEUTRALS'), + SizedBox(height: spacing.md), + // White variants + const _NeutralStrip( + title: 'White', + colors: [ + ('white', StreamColors.white, '100%'), + ('white70', StreamColors.white70, '70%'), + ('white50', StreamColors.white50, '50%'), + ('white20', StreamColors.white20, '20%'), + ('white10', StreamColors.white10, '10%'), + ], + ), + SizedBox(height: spacing.sm), + // Black variants + const _NeutralStrip( + title: 'Black', + colors: [ + ('black', StreamColors.black, '100%'), + ('black50', StreamColors.black50, '50%'), + ('black10', StreamColors.black10, '10%'), + ('black5', StreamColors.black5, '5%'), + ], + ), + SizedBox(height: spacing.sm), + // Transparent + const _TransparentTile(), + ], + ); + } +} + +class _NeutralStrip extends StatelessWidget { + const _NeutralStrip({ + required this.title, + required this.colors, + }); + + final String title; + final List<(String, Color, String)> colors; + + @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( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), + boxShadow: boxShadow.elevation1, + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), + border: Border.all(color: colorScheme.borderSurfaceSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: spacing.sm, vertical: spacing.sm), + color: colorScheme.backgroundSurfaceSubtle, + child: Text( + title, + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textSecondary, + ), + ), + ), + // Colors row + Row( + children: colors.map((entry) { + final (name, color, opacity) = entry; + final brightness = ThemeData.estimateBrightnessForColor(color); + final textColor = brightness == Brightness.dark ? Colors.white : Colors.black; + + return Expanded( + child: InkWell( + onTap: () { + Clipboard.setData( + ClipboardData(text: 'StreamColors.$name'), + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Copied: StreamColors.$name'), + duration: const Duration(seconds: 1), + behavior: SnackBarBehavior.floating, + ), + ); + }, + child: Container( + height: 64, + color: color, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + name, + style: textTheme.metadataEmphasis.copyWith( + color: textColor, + fontFamily: 'monospace', + fontSize: 9, + ), + ), + SizedBox(height: spacing.xxs), + Text( + opacity, + style: textTheme.metadataDefault.copyWith( + color: textColor.withValues(alpha: 0.7), + fontSize: 9, + ), + ), + ], + ), + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } +} + +class _TransparentTile extends StatelessWidget { + const _TransparentTile(); + + @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 InkWell( + onTap: () { + Clipboard.setData( + const ClipboardData(text: 'StreamColors.transparent'), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Copied: StreamColors.transparent'), + duration: Duration(seconds: 1), + behavior: SnackBarBehavior.floating, + ), + ); + }, + borderRadius: BorderRadius.all(radius.lg), + child: Container( + padding: EdgeInsets.all(spacing.md), + decoration: BoxDecoration( + color: StreamColors.transparent, + borderRadius: BorderRadius.all(radius.lg), + boxShadow: boxShadow.elevation1, + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), + border: Border.all( + color: colorScheme.borderSurface, + ), + ), + child: Row( + children: [ + // Checkerboard preview + Container( + width: 40, + height: 40, + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.md), + border: Border.all(color: colorScheme.borderSurfaceSubtle), + ), + child: ClipRRect( + borderRadius: BorderRadius.all(radius.md), + child: CustomPaint( + painter: _CheckerboardPainter( + color: colorScheme.borderSurfaceSubtle, + ), + ), + ), + ), + SizedBox(width: spacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'StreamColors.transparent', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + fontFamily: 'monospace', + ), + ), + SizedBox(width: spacing.xs + spacing.xxs), + Icon( + Icons.copy, + size: 12, + color: colorScheme.textTertiary, + ), + ], + ), + SizedBox(height: spacing.xxs), + Text( + '0% opacity - fully transparent', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textTertiary, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _CheckerboardPainter extends CustomPainter { + const _CheckerboardPainter({required this.color}); + + final Color color; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + const squareSize = 8.0; + for (var i = 0; i < size.width / squareSize; i++) { + for (var j = 0; j < size.height / squareSize; j++) { + if ((i + j).isEven) { + canvas.drawRect( + Rect.fromLTWH(i * squareSize, j * squareSize, squareSize, squareSize), + paint, + ); + } + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +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, + ), + ), + ); + } +} + +class _SwatchData { + const _SwatchData({ + required this.name, + required this.swatch, + required this.usage, + }); + + final String name; + final StreamColorSwatch swatch; + final String usage; +} + +/// Returns the appropriate text color for a given shade background, +/// using the same swatch for visual harmony. +/// +/// This follows the industry-standard "on-color" pattern used by Material +/// Design, Carbon (IBM), and Atlassian design systems. Using colors from +/// the same family (rather than pure black/white) creates better visual +/// cohesion while maintaining WCAG contrast requirements. +/// +/// The graduated contrast approach is based on perceptual depth theory— +/// mimicking how light affects surfaces in real-world environments. +/// +/// Light backgrounds (50-500) use darker shades for text. +/// Dark backgrounds (600+) use the lightest shade for text. +Color _getTextColorForShade(StreamColorSwatch swatch, int shade) { + // Graduated contrast scale: + // - shade 50-100: use shade 700 for text (~5:1 contrast) + // - shade 200-300: use shade 800 for text (~6:1 contrast) + // - shade 400-500: use shade 900 for text (~7:1 contrast) + // - shade 600+: use shade 50 for text (inverted for dark backgrounds) + if (shade <= 100) return swatch.shade700; + if (shade <= 300) return swatch.shade800; + if (shade <= 500) return swatch.shade900; + return swatch.shade50; +} diff --git a/apps/design_system_gallery/lib/primitives/radius.dart b/apps/design_system_gallery/lib/primitives/radius.dart new file mode 100644 index 0000000..0457f10 --- /dev/null +++ b/apps/design_system_gallery/lib/primitives/radius.dart @@ -0,0 +1,370 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart'; + +@UseCase( + name: 'All Values', + type: StreamRadius, + path: '[App Foundation]/Primitives/Radius', +) +Widget buildStreamRadiusShowcase(BuildContext context) { + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + 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: [ + // All radius values + const _RadiusCardsList(), + SizedBox(height: spacing.xl), + + // Quick reference + const _QuickReference(), + ], + ), + ), + ); +} + +/// Full-width radius cards showing all values +class _RadiusCardsList extends StatelessWidget { + const _RadiusCardsList(); + + @override + Widget build(BuildContext context) { + final radius = context.streamRadius; + final spacing = context.streamSpacing; + + final allRadii = [ + _RadiusData(name: 'none', radius: radius.none, usage: 'Sharp corners, dividers'), + _RadiusData(name: 'xxs', radius: radius.xxs, usage: 'Minimal softening'), + _RadiusData(name: 'xs', radius: radius.xs, usage: 'Badges, tags'), + _RadiusData(name: 'sm', radius: radius.sm, usage: 'Small buttons, chips'), + _RadiusData(name: 'md', radius: radius.md, usage: 'Default buttons, inputs'), + _RadiusData(name: 'lg', radius: radius.lg, usage: 'Cards, containers'), + _RadiusData(name: 'xl', radius: radius.xl, usage: 'Modals, large cards'), + _RadiusData(name: 'xxl', radius: radius.xxl, usage: 'Sheets, dialogs'), + _RadiusData(name: 'xxxl', radius: radius.xxxl, usage: 'Large containers'), + _RadiusData(name: 'xxxxl', radius: radius.xxxxl, usage: 'Hero sections'), + _RadiusData(name: 'max', radius: radius.max, usage: 'Pills, avatars, FABs'), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'RADIUS SCALE'), + SizedBox(height: spacing.md), + ...allRadii.map((data) => _RadiusCard(data: data)), + ], + ); + } +} + +class _RadiusCard extends StatelessWidget { + const _RadiusCard({required this.data}); + + final _RadiusData data; + + @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; + + final value = data.radius.x; + final displayValue = value == 9999 ? 'max' : '${value.toStringAsFixed(0)}px'; + + return Padding( + padding: EdgeInsets.only(bottom: spacing.sm), + child: InkWell( + onTap: () { + Clipboard.setData(ClipboardData(text: 'radius.${data.name}')); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Copied: radius.${data.name}'), + duration: const Duration(seconds: 1), + behavior: SnackBarBehavior.floating, + ), + ); + }, + borderRadius: BorderRadius.all(radius.lg), + child: Container( + 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.borderSurfaceSubtle), + ), + child: Row( + children: [ + // Large preview box - rectangle to show radius clearly + Container( + width: 120, + height: 64, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.accentPrimary, + colorScheme.accentPrimary.withValues(alpha: 0.7), + ], + ), + borderRadius: BorderRadius.all(data.radius), + ), + ), + SizedBox(width: spacing.md + spacing.xs), + // Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'radius.${data.name}', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + SizedBox(width: spacing.sm), + Container( + padding: EdgeInsets.symmetric( + horizontal: spacing.xs + spacing.xxs, + vertical: spacing.xxs, + ), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.xs), + ), + child: Text( + displayValue, + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.textSecondary, + fontFamily: 'monospace', + fontSize: 10, + ), + ), + ), + SizedBox(width: spacing.xs + spacing.xxs), + Icon( + Icons.copy, + size: 12, + color: colorScheme.textTertiary, + ), + ], + ), + SizedBox(height: spacing.xs + spacing.xxs), + Text( + data.usage, + style: textTheme.captionDefault.copyWith( + color: colorScheme.textTertiary, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +/// Quick reference for radius usage +class _QuickReference extends StatelessWidget { + const _QuickReference(); + + @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: 'QUICK REFERENCE'), + SizedBox(height: spacing.md), + Container( + width: double.infinity, + padding: EdgeInsets.all(spacing.md), + clipBehavior: Clip.antiAlias, + 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.borderSurfaceSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Usage Pattern', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + SizedBox(height: spacing.sm), + Container( + width: double.infinity, + padding: EdgeInsets.all(spacing.sm), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.md), + ), + child: Text( + 'borderRadius: BorderRadius.all(context.streamRadius.md)', + style: textTheme.captionDefault.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + ), + SizedBox(height: spacing.md), + Divider(color: colorScheme.borderSurfaceSubtle), + SizedBox(height: spacing.md), + Text( + 'Common Choices', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + SizedBox(height: spacing.sm), + const _UsageHint( + token: 'sm', + description: 'Interactive elements (buttons, inputs)', + ), + SizedBox(height: spacing.sm), + const _UsageHint( + token: 'md', + description: 'Medium containers (cards, dropdowns)', + ), + SizedBox(height: spacing.sm), + const _UsageHint( + token: 'lg', + description: 'Large containers (modals, sheets)', + ), + SizedBox(height: spacing.sm), + const _UsageHint( + token: 'max', + description: 'Circular elements (avatars, pills)', + ), + ], + ), + ), + ], + ); + } +} + +class _UsageHint extends StatelessWidget { + const _UsageHint({ + required this.token, + required this.description, + }); + + final String token; + final String description; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + + return Row( + children: [ + Container( + width: 48, + padding: EdgeInsets.symmetric(vertical: spacing.xxs), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.xs), + ), + child: Center( + child: Text( + token, + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + ), + ), + SizedBox(width: spacing.sm), + Expanded( + child: Text( + description, + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + ), + ], + ); + } +} + +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, + ), + ), + ); + } +} + +class _RadiusData { + const _RadiusData({ + required this.name, + required this.radius, + required this.usage, + }); + + final String name; + final Radius radius; + final String usage; +} diff --git a/apps/design_system_gallery/lib/primitives/spacing.dart b/apps/design_system_gallery/lib/primitives/spacing.dart new file mode 100644 index 0000000..08f889b --- /dev/null +++ b/apps/design_system_gallery/lib/primitives/spacing.dart @@ -0,0 +1,401 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart'; + +@UseCase( + name: 'All Values', + type: StreamSpacing, + path: '[App Foundation]/Primitives/Spacing', +) +Widget buildStreamSpacingShowcase(BuildContext context) { + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + 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: [ + // All spacing values + const _SpacingCardsList(), + SizedBox(height: spacing.xl), + + // Quick reference + const _QuickReference(), + ], + ), + ), + ); +} + +/// Full-width spacing cards showing all values +class _SpacingCardsList extends StatelessWidget { + const _SpacingCardsList(); + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + final allSpacing = [ + _SpacingData(name: 'none', value: spacing.none, usage: 'No spacing, tight joins'), + _SpacingData(name: 'xxs', value: spacing.xxs, usage: 'Minimal gaps (icon+text)'), + _SpacingData(name: 'xs', value: spacing.xs, usage: 'Inline elements, small gaps'), + _SpacingData(name: 'sm', value: spacing.sm, usage: 'Button padding, list gaps'), + _SpacingData(name: 'md', value: spacing.md, usage: 'Default padding, sections'), + _SpacingData(name: 'lg', value: spacing.lg, usage: 'Large padding, groups'), + _SpacingData(name: 'xl', value: spacing.xl, usage: 'Section spacing'), + _SpacingData(name: 'xxl', value: spacing.xxl, usage: 'Modal padding, gutters'), + _SpacingData(name: 'xxxl', value: spacing.xxxl, usage: 'Page margins, hero gaps'), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'SPACING SCALE'), + SizedBox(height: spacing.md), + ...allSpacing.map((data) => _SpacingCard(data: data)), + ], + ); + } +} + +class _SpacingCard extends StatelessWidget { + const _SpacingCard({required this.data}); + + final _SpacingData data; + + @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 Padding( + padding: EdgeInsets.only(bottom: spacing.sm), + child: InkWell( + onTap: () { + Clipboard.setData(ClipboardData(text: 'spacing.${data.name}')); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Copied: spacing.${data.name}'), + duration: const Duration(seconds: 1), + behavior: SnackBarBehavior.floating, + ), + ); + }, + borderRadius: BorderRadius.all(radius.lg), + child: Container( + 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.borderSurfaceSubtle), + ), + child: Row( + children: [ + // Visual spacing demonstration - two boxes with the actual gap + Container( + width: 120, + height: 64, + padding: EdgeInsets.all(spacing.sm), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.md), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 24, + height: 48, + decoration: BoxDecoration( + color: colorScheme.accentPrimary, + borderRadius: BorderRadius.all(radius.xs), + ), + ), + // The actual spacing + SizedBox(width: data.value.clamp(0, 40)), + Container( + width: 24, + height: 48, + decoration: BoxDecoration( + color: colorScheme.accentPrimary, + borderRadius: BorderRadius.all(radius.xs), + ), + ), + ], + ), + ), + SizedBox(width: spacing.md + spacing.xs), + // Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'spacing.${data.name}', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + SizedBox(width: spacing.sm), + Container( + padding: EdgeInsets.symmetric( + horizontal: spacing.xs + spacing.xxs, + vertical: spacing.xxs, + ), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.xs), + ), + child: Text( + '${data.value.toInt()}px', + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.textSecondary, + fontFamily: 'monospace', + fontSize: 10, + ), + ), + ), + SizedBox(width: spacing.xs + spacing.xxs), + Icon( + Icons.copy, + size: 12, + color: colorScheme.textTertiary, + ), + ], + ), + SizedBox(height: spacing.xs + spacing.xxs), + Text( + data.usage, + style: textTheme.captionDefault.copyWith( + color: colorScheme.textTertiary, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +/// Quick reference for spacing usage +class _QuickReference extends StatelessWidget { + const _QuickReference(); + + @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: 'QUICK REFERENCE'), + SizedBox(height: spacing.md), + Container( + width: double.infinity, + padding: EdgeInsets.all(spacing.md), + clipBehavior: Clip.antiAlias, + 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.borderSurfaceSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Usage Patterns', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + SizedBox(height: spacing.sm), + Container( + width: double.infinity, + padding: EdgeInsets.all(spacing.sm), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.md), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'padding: EdgeInsets.all(context.streamSpacing.md)', + style: textTheme.metadataDefault.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + SizedBox(height: spacing.xs), + Text( + 'SizedBox(height: context.streamSpacing.lg)', + style: textTheme.metadataDefault.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + SizedBox(height: spacing.md), + Divider(color: colorScheme.borderSurfaceSubtle), + SizedBox(height: spacing.md), + Text( + '8px Grid System', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + SizedBox(height: spacing.sm), + Text( + 'All spacing values follow a consistent 4/8px grid for visual harmony.', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.sm), + const _UsageHint( + token: 'xxs-xs', + description: 'Fine adjustments (2-4px)', + ), + SizedBox(height: spacing.sm), + const _UsageHint( + token: 'sm-md', + description: 'Component internals (8-16px)', + ), + SizedBox(height: spacing.sm), + const _UsageHint( + token: 'lg-xl', + description: 'Section gaps (24-32px)', + ), + SizedBox(height: spacing.sm), + const _UsageHint( + token: 'xxl-xxxl', + description: 'Page-level spacing (48-64px)', + ), + ], + ), + ), + ], + ); + } +} + +class _UsageHint extends StatelessWidget { + const _UsageHint({ + required this.token, + required this.description, + }); + + final String token; + final String description; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + + return Row( + children: [ + Container( + width: 56, + padding: EdgeInsets.symmetric(vertical: spacing.xxs), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.xs), + ), + child: Center( + child: Text( + token, + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + ), + ), + SizedBox(width: spacing.sm), + Expanded( + child: Text( + description, + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + ), + ], + ); + } +} + +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, + ), + ), + ); + } +} + +class _SpacingData { + const _SpacingData({ + required this.name, + required this.value, + required this.usage, + }); + + final String name; + final double value; + final String usage; +} diff --git a/apps/design_system_gallery/lib/semantics/elevations.dart b/apps/design_system_gallery/lib/semantics/elevations.dart index 2d4cc37..e6e05e9 100644 --- a/apps/design_system_gallery/lib/semantics/elevations.dart +++ b/apps/design_system_gallery/lib/semantics/elevations.dart @@ -6,57 +6,30 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart'; @UseCase( name: 'All Elevations', type: StreamBoxShadow, - path: '[App Foundation]/Elevations', + path: '[App Foundation]/Semantics/Elevations', ) Widget buildStreamBoxShadowShowcase(BuildContext context) { - final streamTheme = StreamTheme.of(context); - final boxShadow = streamTheme.boxShadow; - final colorScheme = streamTheme.colorScheme; - final textTheme = streamTheme.textTheme; + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; return DefaultTextStyle( - style: TextStyle(color: colorScheme.textPrimary), + style: textTheme.bodyDefault.copyWith(color: colorScheme.textPrimary), child: SingleChildScrollView( - padding: const EdgeInsets.all(24), + padding: EdgeInsets.all(spacing.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header - Text( - 'Elevation System', - style: textTheme.headingMd.copyWith(color: colorScheme.textPrimary), - ), - const SizedBox(height: 4), - Text( - 'Shadows create depth hierarchy and visual separation between surfaces', - style: textTheme.bodyDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - const SizedBox(height: 24), - // 3D Stacked visualization - _StackedElevationDemo( - boxShadow: boxShadow, - colorScheme: colorScheme, - textTheme: textTheme, - ), - const SizedBox(height: 32), + const _StackedElevationDemo(), + SizedBox(height: spacing.xl), - // Elevation cards grid - _ElevationGrid( - boxShadow: boxShadow, - colorScheme: colorScheme, - textTheme: textTheme, - ), - const SizedBox(height: 32), + // Elevation cards + const _ElevationGrid(), + SizedBox(height: spacing.xl), - // Technical details - _TechnicalDetails( - boxShadow: boxShadow, - colorScheme: colorScheme, - textTheme: textTheme, - ), + // Quick reference + const _QuickReference(), ], ), ), @@ -64,120 +37,153 @@ Widget buildStreamBoxShadowShowcase(BuildContext context) { } class _StackedElevationDemo extends StatelessWidget { - const _StackedElevationDemo({ - required this.boxShadow, - required this.colorScheme, - required this.textTheme, - }); - - final StreamBoxShadow boxShadow; - final StreamColorScheme colorScheme; - final StreamTextTheme textTheme; + const _StackedElevationDemo(); @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: colorScheme.backgroundSurfaceSubtle, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: colorScheme.borderSurfaceSubtle), - ), - child: Column( - children: [ - Row( + final boxShadow = context.streamBoxShadow; + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'DEPTH HIERARCHY'), + SizedBox(height: spacing.md), + Container( + width: double.infinity, + padding: EdgeInsets.all(spacing.lg), + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.backgroundSurface, + colorScheme.backgroundSurfaceSubtle, + ], + ), + borderRadius: BorderRadius.all(radius.lg), + boxShadow: boxShadow.elevation1, + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), + border: Border.all(color: colorScheme.borderSurfaceSubtle), + ), + child: Column( children: [ - Icon( - Icons.layers_outlined, - size: 16, - color: colorScheme.textSecondary, - ), - const SizedBox(width: 8), - Text( - 'Depth Hierarchy', - style: textTheme.captionEmphasis.copyWith( - color: colorScheme.textSecondary, + SizedBox( + height: 200, + child: Stack( + alignment: Alignment.center, + children: [ + // Base layer + Positioned( + bottom: 0, + child: _buildLayer( + context, + 'elevation1', + boxShadow.elevation1, + 180, + colorScheme.backgroundSurface.withValues(alpha: 0.5), + ), + ), + // elevation2 + Positioned( + bottom: 35, + child: _buildLayer( + context, + 'elevation2', + boxShadow.elevation2, + 155, + colorScheme.backgroundSurface.withValues(alpha: 0.7), + ), + ), + // elevation3 + Positioned( + bottom: 70, + child: _buildLayer( + context, + 'elevation3', + boxShadow.elevation3, + 130, + colorScheme.backgroundSurface.withValues(alpha: 0.85), + ), + ), + // elevation4 + Positioned( + bottom: 105, + child: _buildLayer( + context, + 'elevation4', + boxShadow.elevation4, + 105, + colorScheme.backgroundSurface, + ), + ), + ], ), ), - ], - ), - const SizedBox(height: 20), - SizedBox( - height: 180, - child: Stack( - alignment: Alignment.center, - children: [ - // Base layer - Positioned( - bottom: 0, - child: _buildLayer( - 'Base', - boxShadow.elevation1, - 160, - colorScheme.backgroundSurface.withValues(alpha: 0.6), + SizedBox(height: spacing.md + spacing.xs), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.arrow_downward, + size: 14, + color: colorScheme.textTertiary, ), - ), - // elevation2 - Positioned( - bottom: 30, - child: _buildLayer( - 'elevation2', - boxShadow.elevation2, - 140, - colorScheme.backgroundSurface.withValues(alpha: 0.8), + SizedBox(width: spacing.sm), + Text( + 'Further from viewer', + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + ), ), - ), - // elevation3 - Positioned( - bottom: 60, - child: _buildLayer( - 'elevation3', - boxShadow.elevation3, - 120, - colorScheme.backgroundSurface.withValues(alpha: 0.9), + SizedBox(width: spacing.lg), + Icon( + Icons.arrow_upward, + size: 14, + color: colorScheme.textTertiary, ), - ), - // elevation4 - Positioned( - bottom: 90, - child: _buildLayer( - 'elevation4', - boxShadow.elevation4, - 100, - colorScheme.backgroundSurface, + SizedBox(width: spacing.sm), + Text( + 'Closer to viewer', + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + ), ), - ), - ], - ), - ), - const SizedBox(height: 16), - Text( - 'Higher elevations appear closer to the viewer', - style: textTheme.metadataDefault.copyWith( - color: colorScheme.textTertiary, - fontStyle: FontStyle.italic, - ), + ], + ), + ], ), - ], - ), + ), + ], ); } Widget _buildLayer( + BuildContext context, String label, List shadow, double width, Color color, ) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + return Container( width: width, - height: 50, + height: 56, decoration: BoxDecoration( color: color, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), boxShadow: shadow, border: Border.all( - color: colorScheme.borderSurfaceSubtle.withValues(alpha: 0.5), + color: colorScheme.borderSurfaceSubtle.withValues(alpha: 0.3), ), ), child: Center( @@ -185,6 +191,7 @@ class _StackedElevationDemo extends StatelessWidget { label, style: textTheme.metadataEmphasis.copyWith( color: colorScheme.textSecondary, + fontFamily: 'monospace', ), ), ), @@ -193,44 +200,39 @@ class _StackedElevationDemo extends StatelessWidget { } class _ElevationGrid extends StatelessWidget { - const _ElevationGrid({ - required this.boxShadow, - required this.colorScheme, - required this.textTheme, - }); - - final StreamBoxShadow boxShadow; - final StreamColorScheme colorScheme; - final StreamTextTheme textTheme; + const _ElevationGrid(); @override Widget build(BuildContext context) { + final boxShadow = context.streamBoxShadow; + final spacing = context.streamSpacing; + final elevations = [ _ElevationData( name: 'elevation1', shadow: boxShadow.elevation1, - icon: Icons.layers_outlined, + level: '1', description: 'Subtle lift for resting state', useCases: ['Cards', 'List items', 'Input fields'], ), _ElevationData( name: 'elevation2', shadow: boxShadow.elevation2, - icon: Icons.flip_to_front, + level: '2', description: 'Moderate lift for hover/focus', useCases: ['Dropdowns', 'Menus', 'Popovers'], ), _ElevationData( name: 'elevation3', shadow: boxShadow.elevation3, - icon: Icons.picture_in_picture, + level: '3', description: 'High lift for prominent UI', useCases: ['Modals', 'Dialogs', 'Drawers'], ), _ElevationData( name: 'elevation4', shadow: boxShadow.elevation4, - icon: Icons.web_asset, + level: '4', description: 'Highest lift for alerts', useCases: ['Toasts', 'Notifications', 'Snackbars'], ), @@ -239,40 +241,10 @@ class _ElevationGrid extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: colorScheme.accentPrimary, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - 'LEVELS', - style: TextStyle( - fontSize: 9, - fontWeight: FontWeight.w700, - letterSpacing: 1, - color: colorScheme.textOnAccent, - ), - ), - ), - const SizedBox(width: 10), - Text( - 'Tap to copy usage code', - style: textTheme.captionDefault.copyWith( - color: colorScheme.textTertiary, - ), - ), - ], - ), - const SizedBox(height: 12), + const _SectionLabel(label: 'ELEVATION LEVELS'), + SizedBox(height: spacing.md), ...elevations.map( - (e) => _ElevationCard( - data: e, - colorScheme: colorScheme, - textTheme: textTheme, - ), + (e) => _ElevationCard(data: e), ), ], ); @@ -280,24 +252,24 @@ class _ElevationGrid extends StatelessWidget { } class _ElevationCard extends StatelessWidget { - const _ElevationCard({ - required this.data, - required this.colorScheme, - required this.textTheme, - }); + const _ElevationCard({required this.data}); final _ElevationData data; - final StreamColorScheme colorScheme; - final StreamTextTheme textTheme; @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 Padding( - padding: const EdgeInsets.only(bottom: 10), + padding: EdgeInsets.only(bottom: spacing.sm), child: InkWell( onTap: () { Clipboard.setData( - ClipboardData(text: 'boxShadow: streamTheme.boxShadow.${data.name}'), + ClipboardData(text: 'boxShadow.${data.name}'), ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -307,89 +279,108 @@ class _ElevationCard extends StatelessWidget { ), ); }, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.all(radius.lg), child: Container( - padding: const EdgeInsets.all(14), + clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.all(radius.lg), + boxShadow: boxShadow.elevation1, + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), border: Border.all(color: colorScheme.borderSurfaceSubtle), ), child: Row( children: [ - // Preview box with shadow + // Level indicator Container( - width: 56, - height: 56, + width: 48, + height: 100, decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(10), - boxShadow: data.shadow, + color: colorScheme.backgroundSurfaceSubtle, ), - child: Icon( - data.icon, - color: colorScheme.textTertiary, - size: 24, + child: Center( + child: Text( + data.level, + style: textTheme.headingLg.copyWith( + color: colorScheme.textTertiary, + ), + ), + ), + ), + // Preview box with shadow + Padding( + padding: EdgeInsets.all(spacing.md), + child: Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: colorScheme.backgroundSurface, + borderRadius: BorderRadius.all(radius.lg), + boxShadow: data.shadow, + ), ), ), - const SizedBox(width: 16), // Info Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - data.name, - style: TextStyle( - fontFamily: 'monospace', - fontSize: 12, - fontWeight: FontWeight.w600, - color: colorScheme.textPrimary, + child: Padding( + padding: EdgeInsets.symmetric(vertical: spacing.sm), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + data.name, + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), ), + SizedBox(width: spacing.xs + spacing.xxs), + Icon( + Icons.copy, + size: 11, + color: colorScheme.textTertiary, + ), + ], + ), + SizedBox(height: spacing.xs), + Text( + data.description, + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, ), - const SizedBox(width: 6), - Icon( - Icons.copy, - size: 11, - color: colorScheme.textTertiary, - ), - ], - ), - const SizedBox(height: 4), - Text( - data.description, - style: textTheme.captionDefault.copyWith( - color: colorScheme.textSecondary, ), - ), - const SizedBox(height: 6), - Wrap( - spacing: 6, - runSpacing: 4, - children: data.useCases.map((use) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: colorScheme.backgroundSurfaceSubtle, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - use, - style: textTheme.metadataDefault.copyWith( - color: colorScheme.textTertiary, + SizedBox(height: spacing.sm), + Wrap( + spacing: spacing.xs + spacing.xxs, + runSpacing: spacing.xs, + children: data.useCases.map((use) { + return Container( + padding: EdgeInsets.symmetric( + horizontal: spacing.xs + spacing.xxs, + vertical: spacing.xxs, ), - ), - ); - }).toList(), - ), - ], + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.xs), + ), + child: Text( + use, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + ), + ), + ); + }).toList(), + ), + ], + ), ), ), + SizedBox(width: spacing.sm), ], ), ), @@ -398,85 +389,84 @@ class _ElevationCard extends StatelessWidget { } } -class _TechnicalDetails extends StatelessWidget { - const _TechnicalDetails({ - required this.boxShadow, - required this.colorScheme, - required this.textTheme, - }); - - final StreamBoxShadow boxShadow; - final StreamColorScheme colorScheme; - final StreamTextTheme textTheme; +/// Quick reference for elevation usage +class _QuickReference extends StatelessWidget { + const _QuickReference(); @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: [ - Row( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: colorScheme.accentNeutral, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - 'TECHNICAL', - style: TextStyle( - fontSize: 9, - fontWeight: FontWeight.w700, - letterSpacing: 1, - color: colorScheme.textOnAccent, - ), - ), - ), - const SizedBox(width: 10), - Text( - 'Shadow values for each elevation', - style: textTheme.captionDefault.copyWith( - color: colorScheme.textTertiary, - ), - ), - ], - ), - const SizedBox(height: 12), + const _SectionLabel(label: 'QUICK REFERENCE'), + SizedBox(height: spacing.md), Container( - padding: const EdgeInsets.all(14), + width: double.infinity, + padding: EdgeInsets.all(spacing.md), + clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.all(radius.lg), + boxShadow: boxShadow.elevation1, + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), border: Border.all(color: colorScheme.borderSurfaceSubtle), ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _ShadowRow( - name: 'elevation1', - shadows: boxShadow.elevation1, - colorScheme: colorScheme, - textTheme: textTheme, + Text( + 'Usage Pattern', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + SizedBox(height: spacing.sm), + Container( + width: double.infinity, + padding: EdgeInsets.all(spacing.sm), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.md), + ), + child: Text( + 'boxShadow: context.streamBoxShadow.elevation{1-4}', + style: textTheme.captionDefault.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + ), + SizedBox(height: spacing.md), + Divider(color: colorScheme.borderSurfaceSubtle), + SizedBox(height: spacing.md), + Text( + 'Best Practices', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), ), - Divider(color: colorScheme.borderSurfaceSubtle, height: 20), - _ShadowRow( - name: 'elevation2', - shadows: boxShadow.elevation2, - colorScheme: colorScheme, - textTheme: textTheme, + SizedBox(height: spacing.sm), + const _BestPractice( + icon: Icons.check_circle_outline, + text: 'Use consistent elevation for same-level components', ), - Divider(color: colorScheme.borderSurfaceSubtle, height: 20), - _ShadowRow( - name: 'elevation3', - shadows: boxShadow.elevation3, - colorScheme: colorScheme, - textTheme: textTheme, + SizedBox(height: spacing.sm), + const _BestPractice( + icon: Icons.check_circle_outline, + text: 'Increase elevation for overlapping/modal content', ), - Divider(color: colorScheme.borderSurfaceSubtle, height: 20), - _ShadowRow( - name: 'elevation4', - shadows: boxShadow.elevation4, - colorScheme: colorScheme, - textTheme: textTheme, + SizedBox(height: spacing.sm), + const _BestPractice( + icon: Icons.check_circle_outline, + text: 'Reserve higher elevations for temporary UI', ), ], ), @@ -486,87 +476,84 @@ class _TechnicalDetails extends StatelessWidget { } } -class _ShadowRow extends StatelessWidget { - const _ShadowRow({ - required this.name, - required this.shadows, - required this.colorScheme, - required this.textTheme, +class _BestPractice extends StatelessWidget { + const _BestPractice({ + required this.icon, + required this.text, }); - final String name; - final List shadows; - final StreamColorScheme colorScheme; - final StreamTextTheme textTheme; + final IconData icon; + final String text; @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Row( children: [ - Text( - name, - style: TextStyle( - fontFamily: 'monospace', - fontSize: 11, - fontWeight: FontWeight.w600, - color: colorScheme.textPrimary, - ), + Icon( + icon, + size: 14, + color: colorScheme.accentSuccess, ), - const SizedBox(height: 6), - ...shadows.asMap().entries.map((entry) { - final shadow = entry.value; - final hex = shadow.color.toARGB32().toRadixString(16).toUpperCase(); - return Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row( - children: [ - Container( - width: 12, - height: 12, - decoration: BoxDecoration( - color: shadow.color, - borderRadius: BorderRadius.circular(2), - border: Border.all( - color: colorScheme.borderSurface.withValues(alpha: 0.3), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'offset(${shadow.offset.dx.toInt()}, ${shadow.offset.dy.toInt()}) ' - 'blur(${shadow.blurRadius.toInt()}) ' - 'spread(${shadow.spreadRadius.toInt()}) ' - '#$hex', - style: TextStyle( - fontFamily: 'monospace', - fontSize: 10, - color: colorScheme.textTertiary, - ), - ), - ), - ], + SizedBox(width: spacing.sm), + Expanded( + child: Text( + text, + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, ), - ); - }), + ), + ), ], ); } } +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, + ), + ), + ); + } +} + class _ElevationData { const _ElevationData({ required this.name, required this.shadow, - required this.icon, + required this.level, required this.description, required this.useCases, }); final String name; final List shadow; - final IconData icon; + final String level; final String description; final List useCases; } diff --git a/apps/design_system_gallery/lib/semantics/typography.dart b/apps/design_system_gallery/lib/semantics/typography.dart index 34be88d..717edcb 100644 --- a/apps/design_system_gallery/lib/semantics/typography.dart +++ b/apps/design_system_gallery/lib/semantics/typography.dart @@ -6,26 +6,26 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart'; @UseCase( name: 'All Styles', type: StreamTextTheme, - path: '[App Foundation]/Typography', + path: '[App Foundation]/Semantics/Typography', ) Widget buildStreamTextThemeShowcase(BuildContext context) { - final streamTheme = StreamTheme.of(context); - final textTheme = streamTheme.textTheme; - final colorScheme = streamTheme.colorScheme; + final spacing = context.streamSpacing; return DefaultTextStyle( - style: TextStyle(color: colorScheme.textPrimary), + style: context.streamTextTheme.bodyDefault.copyWith( + color: context.streamColorScheme.textPrimary, + ), child: SingleChildScrollView( - padding: const EdgeInsets.all(24), + padding: EdgeInsets.all(spacing.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Visual Type Scale - _TypeScale(textTheme: textTheme, colorScheme: colorScheme), - const SizedBox(height: 32), + const _TypeScale(), + SizedBox(height: spacing.xl), // Complete Reference - _CompleteReference(textTheme: textTheme, colorScheme: colorScheme), + const _CompleteReference(), ], ), ), @@ -34,304 +34,164 @@ Widget buildStreamTextThemeShowcase(BuildContext context) { /// Visual type scale showing hierarchy at a glance class _TypeScale extends StatelessWidget { - const _TypeScale({required this.textTheme, required this.colorScheme}); - - final StreamTextTheme textTheme; - final StreamColorScheme colorScheme; + const _TypeScale(); @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _SectionLabel(label: 'TYPE SCALE', colorScheme: colorScheme), - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorScheme.backgroundSurface, - colorScheme.backgroundSurfaceSubtle, - ], - ), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: colorScheme.borderSurfaceSubtle), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _ScaleItem( - label: 'Heading Lg', - style: textTheme.headingLg, - colorScheme: colorScheme, - ), - const SizedBox(height: 12), - _ScaleItem( - label: 'Heading Md', - style: textTheme.headingMd, - colorScheme: colorScheme, - ), - const SizedBox(height: 12), - _ScaleItem( - label: 'Heading Sm', - style: textTheme.headingSm, - colorScheme: colorScheme, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Divider(color: colorScheme.borderSurfaceSubtle), - ), - _ScaleItem( - label: 'Body', - style: textTheme.bodyDefault, - colorScheme: colorScheme, - ), - const SizedBox(height: 12), - _ScaleItem( - label: 'Caption', - style: textTheme.captionDefault, - colorScheme: colorScheme, - ), - const SizedBox(height: 12), - _ScaleItem( - label: 'Metadata', - style: textTheme.metadataDefault, - colorScheme: colorScheme, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Divider(color: colorScheme.borderSurfaceSubtle), - ), - _ScaleItem( - label: 'Numeric Lg', - style: textTheme.numericLg, - colorScheme: colorScheme, - sampleText: '1,234', - ), - const SizedBox(height: 12), - _ScaleItem( - label: 'Numeric Md', - style: textTheme.numericMd, - colorScheme: colorScheme, - sampleText: '99+', - ), - const SizedBox(height: 12), - _ScaleItem( - label: 'Numeric Sm', - style: textTheme.numericSm, - colorScheme: colorScheme, - sampleText: '5', - ), - ], - ), - ), - ], - ); - } -} - -class _ScaleItem extends StatelessWidget { - const _ScaleItem({ - required this.label, - required this.style, - required this.colorScheme, - this.sampleText, - }); - - final String label; - final TextStyle style; - final StreamColorScheme colorScheme; - final String? sampleText; + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final boxShadow = context.streamBoxShadow; + final radius = context.streamRadius; + final spacing = context.streamSpacing; - @override - Widget build(BuildContext context) { - final size = style.fontSize?.toInt() ?? 0; - return Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - SizedBox( - width: 32, - child: Text( - '$size', - style: TextStyle( - fontFamily: 'monospace', - fontSize: 10, - color: colorScheme.textTertiary, - ), - ), - ), - Container( - width: 3, - height: size.toDouble().clamp(8, 40), - margin: const EdgeInsets.only(right: 12), - decoration: BoxDecoration( - color: colorScheme.accentPrimary, - borderRadius: BorderRadius.circular(2), - ), - ), - Expanded( - child: Text( - sampleText ?? 'The quick brown fox jumps over', - style: style.copyWith(color: colorScheme.textPrimary), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 8), - Text( - label, - style: TextStyle( - fontSize: 9, - color: colorScheme.textTertiary, - fontWeight: FontWeight.w500, - ), - ), - ], - ); - } -} - -/// Complete reference table -class _CompleteReference extends StatelessWidget { - const _CompleteReference({ - required this.textTheme, - required this.colorScheme, - }); - - final StreamTextTheme textTheme; - final StreamColorScheme colorScheme; - - @override - Widget build(BuildContext context) { - final styles = [ - ('headingLg', textTheme.headingLg, 'Page titles'), - ('headingMd', textTheme.headingMd, 'Section headers'), - ('headingSm', textTheme.headingSm, 'Card titles'), - ('bodyDefault', textTheme.bodyDefault, 'Paragraphs'), - ('bodyEmphasis', textTheme.bodyEmphasis, 'Important text'), - ('bodyLink', textTheme.bodyLink, 'Links'), - ('bodyLinkEmphasis', textTheme.bodyLinkEmphasis, 'Bold links'), - ('captionDefault', textTheme.captionDefault, 'Labels'), - ('captionEmphasis', textTheme.captionEmphasis, 'Bold labels'), - ('captionLink', textTheme.captionLink, 'Small links'), - ('captionLinkEmphasis', textTheme.captionLinkEmphasis, 'Bold small links'), - ('metadataDefault', textTheme.metadataDefault, 'Timestamps'), - ('metadataEmphasis', textTheme.metadataEmphasis, 'Bold metadata'), - ('metadataLink', textTheme.metadataLink, 'Tiny links'), - ('metadataLinkEmphasis', textTheme.metadataLinkEmphasis, 'Bold tiny links'), - ('numericLg', textTheme.numericLg, 'Counters'), - ('numericMd', textTheme.numericMd, 'Badges'), - ('numericSm', textTheme.numericSm, 'Indicators'), + final categories = [ + ( + 'HEADINGS', + 'Page and section titles', + [ + ('headingLg', textTheme.headingLg, 'Page titles, hero text'), + ('headingMd', textTheme.headingMd, 'Section headers'), + ('headingSm', textTheme.headingSm, 'Card titles, dialogs'), + ], + ), + ( + 'BODY', + 'Main content text', + [ + ('bodyDefault', textTheme.bodyDefault, 'Paragraphs, descriptions'), + ('bodyEmphasis', textTheme.bodyEmphasis, 'Important inline text'), + ('bodyLink', textTheme.bodyLink, 'Clickable links'), + ('bodyLinkEmphasis', textTheme.bodyLinkEmphasis, 'Bold links'), + ], + ), + ( + 'CAPTION', + 'Secondary and supporting text', + [ + ('captionDefault', textTheme.captionDefault, 'Labels, hints'), + ('captionEmphasis', textTheme.captionEmphasis, 'Bold labels'), + ('captionLink', textTheme.captionLink, 'Small links'), + ('captionLinkEmphasis', textTheme.captionLinkEmphasis, 'Bold small links'), + ], + ), + ( + 'METADATA', + 'Smallest text for auxiliary info', + [ + ('metadataDefault', textTheme.metadataDefault, 'Timestamps, counts'), + ('metadataEmphasis', textTheme.metadataEmphasis, 'Bold metadata'), + ('metadataLink', textTheme.metadataLink, 'Tiny links'), + ('metadataLinkEmphasis', textTheme.metadataLinkEmphasis, 'Bold tiny links'), + ], + ), + ( + 'NUMERIC', + 'Numbers and counters', + [ + ('numericLg', textTheme.numericLg, 'Large counters, stats'), + ('numericMd', textTheme.numericMd, 'Badges, indicators'), + ('numericSm', textTheme.numericSm, 'Small counts'), + ], + ), ]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _SectionLabel(label: 'REFERENCE', colorScheme: colorScheme), - const SizedBox(height: 16), - Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: colorScheme.borderSurfaceSubtle), - ), - child: Column( - children: [ - // Header - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: colorScheme.backgroundSurfaceSubtle, - borderRadius: const BorderRadius.vertical(top: Radius.circular(11)), - ), - child: Row( - children: [ - Expanded( - flex: 3, - child: Text( - 'Name', - style: textTheme.metadataEmphasis.copyWith( - color: colorScheme.textSecondary, - ), - ), + const _SectionLabel(label: 'TYPE SCALE'), + SizedBox(height: spacing.md), + ...categories.map((category) { + final (title, description, styles) = category; + return Padding( + padding: EdgeInsets.only(bottom: spacing.md), + child: Container( + clipBehavior: Clip.antiAlias, + 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.borderSurfaceSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Category header + Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: spacing.md, + vertical: spacing.sm + spacing.xxs, ), - Expanded( - flex: 2, - child: Text( - 'Size', - style: textTheme.metadataEmphasis.copyWith( - color: colorScheme.textSecondary, - ), - ), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, ), - Expanded( - flex: 2, - child: Text( - 'Weight', - style: textTheme.metadataEmphasis.copyWith( - color: colorScheme.textSecondary, + child: Row( + children: [ + Text( + title, + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.textPrimary, + letterSpacing: 0.5, + ), ), - ), - ), - Expanded( - flex: 2, - child: Text( - 'Use', - style: textTheme.metadataEmphasis.copyWith( - color: colorScheme.textSecondary, + SizedBox(width: spacing.sm), + Text( + '— $description', + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + ), ), - ), + ], ), - ], - ), + ), + // Styles list + ...styles.asMap().entries.map((entry) { + final (name, style, usage) = entry.value; + final isLast = entry.key == styles.length - 1; + return _TypeStyleCard( + name: name, + style: style, + usage: usage, + showBorder: !isLast, + ); + }), + ], ), - // Rows - ...styles.asMap().entries.map((entry) { - final (name, style, usage) = entry.value; - final isLast = entry.key == styles.length - 1; - return _ReferenceRow( - name: name, - style: style, - usage: usage, - textTheme: textTheme, - colorScheme: colorScheme, - showBorder: !isLast, - ); - }), - ], - ), - ), + ), + ); + }), ], ); } } -class _ReferenceRow extends StatelessWidget { - const _ReferenceRow({ +/// Individual type style card +class _TypeStyleCard extends StatelessWidget { + const _TypeStyleCard({ required this.name, required this.style, required this.usage, - required this.textTheme, - required this.colorScheme, required this.showBorder, }); final String name; final TextStyle style; final String usage; - final StreamTextTheme textTheme; - final StreamColorScheme colorScheme; final bool showBorder; @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + final size = style.fontSize?.toInt() ?? 0; final weight = _weightName(style.fontWeight); + final lineHeight = style.height ?? 1.0; return InkWell( onTap: () { @@ -345,60 +205,82 @@ class _ReferenceRow extends StatelessWidget { ); }, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + padding: EdgeInsets.all(spacing.md), decoration: BoxDecoration( - border: showBorder ? Border(bottom: BorderSide(color: colorScheme.borderSurfaceSubtle)) : null, + border: showBorder + ? Border( + bottom: BorderSide(color: colorScheme.borderSurfaceSubtle), + ) + : null, ), child: Row( children: [ + // Size indicator bar + Container( + width: 3, + height: size.toDouble().clamp(12, 32), + margin: EdgeInsets.only(right: spacing.sm), + decoration: BoxDecoration( + color: colorScheme.accentPrimary, + borderRadius: BorderRadius.all(radius.xxs), + ), + ), + // Preview text Expanded( flex: 3, - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - name, - style: TextStyle( - fontFamily: 'monospace', - fontSize: 10, - fontWeight: FontWeight.w500, - color: colorScheme.textPrimary, - ), + name.contains('numeric') ? '1,234,567' : 'The quick brown fox', + style: style.copyWith(color: colorScheme.textPrimary), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - const SizedBox(width: 4), - Icon( - Icons.copy, - size: 10, - color: colorScheme.textTertiary, + SizedBox(height: spacing.xs), + Row( + children: [ + Text( + name, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + SizedBox(width: spacing.xs), + Icon( + Icons.copy, + size: 10, + color: colorScheme.textTertiary, + ), + ], ), ], ), ), + // Specs Expanded( flex: 2, - child: Text( - '${size}px', - style: textTheme.metadataDefault.copyWith( - color: colorScheme.textSecondary, - fontFamily: 'monospace', - ), - ), - ), - Expanded( - flex: 2, - child: Text( - weight, - style: textTheme.metadataDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - ), - Expanded( - flex: 2, - child: Text( - usage, - style: textTheme.metadataDefault.copyWith( - color: colorScheme.textTertiary, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + _SpecChip(label: '${size}px'), + SizedBox(width: spacing.xs + spacing.xxs), + _SpecChip(label: weight), + SizedBox(width: spacing.xs + spacing.xxs), + _SpecChip(label: '${lineHeight.toStringAsFixed(1)}×'), + ], + ), + SizedBox(height: spacing.xs), + Text( + usage, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + ), + ), + ], ), ), ], @@ -409,36 +291,287 @@ class _ReferenceRow extends StatelessWidget { String _weightName(FontWeight? weight) { return switch (weight) { + FontWeight.w100 => 'Thin', + FontWeight.w200 => 'ExtraLight', + FontWeight.w300 => 'Light', FontWeight.w400 => 'Regular', FontWeight.w500 => 'Medium', - FontWeight.w600 => 'Semi', + FontWeight.w600 => 'SemiBold', FontWeight.w700 => 'Bold', + FontWeight.w800 => 'ExtraBold', + FontWeight.w900 => 'Black', _ => '${weight?.value ?? "?"}', }; } } +/// Small spec chip for displaying typography specs +class _SpecChip extends StatelessWidget { + const _SpecChip({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.xs + spacing.xxs, vertical: spacing.xxs), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.xs), + ), + child: Text( + label, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textSecondary, + fontFamily: 'monospace', + fontSize: 9, + ), + ), + ); + } +} + +/// Quick reference summary +class _CompleteReference extends StatelessWidget { + const _CompleteReference(); + + @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: 'QUICK REFERENCE'), + SizedBox(height: spacing.md), + Container( + width: double.infinity, + padding: EdgeInsets.all(spacing.md), + clipBehavior: Clip.antiAlias, + 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.borderSurfaceSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Style Naming Convention', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + SizedBox(height: spacing.sm), + const _ConventionRow( + pattern: '{category}Default', + example: 'bodyDefault', + description: 'Regular weight, base style', + ), + SizedBox(height: spacing.sm), + const _ConventionRow( + pattern: '{category}Emphasis', + example: 'bodyEmphasis', + description: 'SemiBold weight, emphasis', + ), + SizedBox(height: spacing.sm), + const _ConventionRow( + pattern: '{category}Link', + example: 'bodyLink', + description: 'Regular weight, underlined', + ), + SizedBox(height: spacing.sm), + const _ConventionRow( + pattern: '{category}LinkEmphasis', + example: 'bodyLinkEmphasis', + description: 'SemiBold weight, underlined', + ), + SizedBox(height: spacing.md), + Divider(color: colorScheme.borderSurfaceSubtle), + SizedBox(height: spacing.md), + Text( + 'Size Scale', + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + SizedBox(height: spacing.sm), + Wrap( + spacing: spacing.sm, + runSpacing: spacing.sm, + children: const [ + _SizeTag( + label: 'heading', + sizes: '24 / 20 / 18', + ), + _SizeTag( + label: 'body', + sizes: '16', + ), + _SizeTag( + label: 'caption', + sizes: '14', + ), + _SizeTag( + label: 'metadata', + sizes: '12', + ), + _SizeTag( + label: 'numeric', + sizes: '22 / 14 / 10', + ), + ], + ), + ], + ), + ), + ], + ); + } +} + +class _ConventionRow extends StatelessWidget { + const _ConventionRow({ + required this.pattern, + required this.example, + required this.description, + }); + + final String pattern; + final String example; + final String description; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + + return Row( + children: [ + SizedBox( + width: 160, + child: Text( + pattern, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: spacing.xs + spacing.xxs, vertical: spacing.xxs), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.xs), + ), + child: Text( + example, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textSecondary, + fontFamily: 'monospace', + fontSize: 10, + ), + ), + ), + SizedBox(width: spacing.sm), + Expanded( + child: Text( + description, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + ), + ), + ), + ], + ); + } +} + +class _SizeTag extends StatelessWidget { + const _SizeTag({ + required this.label, + required this.sizes, + }); + + final String label; + final String sizes; + + @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 + spacing.xxs, vertical: spacing.xs + spacing.xxs), + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.sm), + border: Border.all(color: colorScheme.borderSurfaceSubtle), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + SizedBox(width: spacing.sm), + Text( + sizes, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + fontFamily: 'monospace', + ), + ), + ], + ), + ); + } +} + class _SectionLabel extends StatelessWidget { - const _SectionLabel({required this.label, required this.colorScheme}); + const _SectionLabel({required this.label}); final String label; - final StreamColorScheme colorScheme; @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: EdgeInsets.symmetric(horizontal: spacing.sm, vertical: spacing.xs), decoration: BoxDecoration( color: colorScheme.accentPrimary, - borderRadius: BorderRadius.circular(4), + borderRadius: BorderRadius.all(radius.xs), ), child: Text( label, - style: TextStyle( - fontSize: 9, - fontWeight: FontWeight.w700, - letterSpacing: 1, + style: textTheme.metadataEmphasis.copyWith( color: colorScheme.textOnAccent, + letterSpacing: 1, + fontSize: 9, ), ), ); diff --git a/apps/design_system_gallery/lib/widgets/theme_studio/avatar_palette_section.dart b/apps/design_system_gallery/lib/widgets/theme_studio/avatar_palette_section.dart index a135cb2..227d8ae 100644 --- a/apps/design_system_gallery/lib/widgets/theme_studio/avatar_palette_section.dart +++ b/apps/design_system_gallery/lib/widgets/theme_studio/avatar_palette_section.dart @@ -8,7 +8,6 @@ class AvatarColorPairTile extends StatelessWidget { super.key, required this.index, required this.pair, - required this.colorScheme, required this.onBackgroundChanged, required this.onForegroundChanged, this.onRemove, @@ -16,24 +15,28 @@ class AvatarColorPairTile extends StatelessWidget { final int index; final StreamAvatarColorPair pair; - final StreamColorScheme colorScheme; final ValueChanged onBackgroundChanged; final ValueChanged onForegroundChanged; final VoidCallback? onRemove; @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + return Padding( - padding: const EdgeInsets.only(bottom: 8), + padding: EdgeInsets.only(bottom: spacing.sm), child: Container( - padding: const EdgeInsets.all(10), + padding: EdgeInsets.all(spacing.sm + spacing.xxs), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), ), foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), border: Border.all(color: colorScheme.borderSurfaceSubtle), ), child: Column( @@ -52,22 +55,18 @@ class AvatarColorPairTile extends StatelessWidget { child: Center( child: Text( 'AB', - style: TextStyle( + style: textTheme.captionEmphasis.copyWith( color: pair.foregroundColor, - fontSize: 12, - fontWeight: FontWeight.w600, ), ), ), ), - const SizedBox(width: 10), + SizedBox(width: spacing.sm + spacing.xxs), Expanded( child: Text( 'Palette ${index + 1}', - style: TextStyle( + style: textTheme.captionEmphasis.copyWith( color: colorScheme.textPrimary, - fontWeight: FontWeight.w600, - fontSize: 11, ), ), ), @@ -85,14 +84,13 @@ class AvatarColorPairTile extends StatelessWidget { ), ], ), - const SizedBox(height: 8), + SizedBox(height: spacing.sm), Row( children: [ Expanded( child: _SmallColorButton( label: 'backgroundColor', color: pair.backgroundColor, - colorScheme: colorScheme, onTap: () => _showColorPicker( context, 'backgroundColor', @@ -101,12 +99,11 @@ class AvatarColorPairTile extends StatelessWidget { ), ), ), - const SizedBox(width: 8), + SizedBox(width: spacing.sm), Expanded( child: _SmallColorButton( label: 'foregroundColor', color: pair.foregroundColor, - colorScheme: colorScheme, onTap: () => _showColorPicker( context, 'foregroundColor', @@ -130,13 +127,16 @@ class AvatarColorPairTile extends StatelessWidget { ValueChanged onChanged, ) async { var pickerColor = initialColor; + final textTheme = context.streamTextTheme; await showDialog( context: context, builder: (context) => AlertDialog( title: Text( label, - style: const TextStyle(fontFamily: 'monospace', fontSize: 16), + style: textTheme.bodyEmphasis.copyWith( + fontFamily: 'monospace', + ), ), content: SingleChildScrollView( child: ColorPicker( @@ -168,29 +168,32 @@ class _SmallColorButton extends StatelessWidget { const _SmallColorButton({ required this.label, required this.color, - required this.colorScheme, required this.onTap, }); final String label; final Color color; - final StreamColorScheme colorScheme; final VoidCallback onTap; @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + return InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.all(radius.sm), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + padding: EdgeInsets.symmetric(horizontal: spacing.sm, vertical: spacing.xs + spacing.xxs), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.all(radius.sm), ), foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.all(radius.sm), border: Border.all(color: colorScheme.borderSurfaceSubtle), ), child: Row( @@ -201,22 +204,21 @@ class _SmallColorButton extends StatelessWidget { clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: color, - borderRadius: BorderRadius.circular(3), + borderRadius: BorderRadius.all(radius.xxs), ), foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(3), + borderRadius: BorderRadius.all(radius.xxs), border: Border.all( color: colorScheme.borderSurface.withValues(alpha: 0.3), ), ), ), - const SizedBox(width: 6), + SizedBox(width: spacing.xs + spacing.xxs), Expanded( child: Text( label, - style: TextStyle( + style: textTheme.metadataDefault.copyWith( color: colorScheme.textSecondary, - fontSize: 9, fontFamily: 'monospace', ), overflow: TextOverflow.ellipsis, @@ -233,39 +235,40 @@ class _SmallColorButton extends StatelessWidget { class AddPaletteButton extends StatelessWidget { const AddPaletteButton({ super.key, - required this.colorScheme, required this.onTap, }); - final StreamColorScheme colorScheme; final VoidCallback onTap; @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + return InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), child: Container( - padding: const EdgeInsets.symmetric(vertical: 10), + padding: EdgeInsets.symmetric(vertical: spacing.sm + spacing.xxs), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), ), foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), border: Border.all(color: colorScheme.borderSurfaceSubtle), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.add, color: colorScheme.accentPrimary, size: 16), - const SizedBox(width: 6), + SizedBox(width: spacing.xs + spacing.xxs), Text( 'Add Palette Entry', - style: TextStyle( + style: textTheme.captionDefault.copyWith( color: colorScheme.accentPrimary, - fontWeight: FontWeight.w500, - fontSize: 11, ), ), ], diff --git a/apps/design_system_gallery/lib/widgets/theme_studio/color_picker_tile.dart b/apps/design_system_gallery/lib/widgets/theme_studio/color_picker_tile.dart index 8474f5a..92211a8 100644 --- a/apps/design_system_gallery/lib/widgets/theme_studio/color_picker_tile.dart +++ b/apps/design_system_gallery/lib/widgets/theme_studio/color_picker_tile.dart @@ -8,33 +8,35 @@ class ColorPickerTile extends StatelessWidget { super.key, required this.label, required this.color, - required this.colorScheme, - required this.boxShadow, required this.onColorChanged, }); final String label; final Color color; - final StreamColorScheme colorScheme; - final StreamBoxShadow boxShadow; final ValueChanged onColorChanged; @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 Padding( - padding: const EdgeInsets.only(bottom: 6), + padding: EdgeInsets.only(bottom: spacing.xs + spacing.xxs), child: InkWell( onTap: () => _showColorPicker(context), - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.all(radius.sm), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + padding: EdgeInsets.symmetric(horizontal: spacing.sm + spacing.xxs, vertical: spacing.sm), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.all(radius.sm), ), foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.all(radius.sm), border: Border.all(color: colorScheme.borderSurfaceSubtle), ), child: Row( @@ -45,37 +47,34 @@ class ColorPickerTile extends StatelessWidget { clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: color, - borderRadius: BorderRadius.circular(4), + borderRadius: BorderRadius.all(radius.xs), boxShadow: boxShadow.elevation1, ), foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), + borderRadius: BorderRadius.all(radius.xs), border: Border.all( color: colorScheme.borderSurface.withValues(alpha: 0.3), ), ), ), - const SizedBox(width: 10), + SizedBox(width: spacing.sm + spacing.xxs), Expanded( child: Text( label, - style: TextStyle( + style: textTheme.captionDefault.copyWith( color: colorScheme.textPrimary, - fontWeight: FontWeight.w500, - fontSize: 11, fontFamily: 'monospace', ), ), ), Text( _colorToHex(color), - style: TextStyle( + style: textTheme.metadataDefault.copyWith( color: colorScheme.textTertiary, - fontSize: 9, fontFamily: 'monospace', ), ), - const SizedBox(width: 6), + SizedBox(width: spacing.xs + spacing.xxs), Icon(Icons.edit, color: colorScheme.textTertiary, size: 12), ], ), @@ -91,13 +90,16 @@ class ColorPickerTile extends StatelessWidget { Future _showColorPicker(BuildContext context) async { var pickerColor = color; + final textTheme = context.streamTextTheme; await showDialog( context: context, builder: (context) => AlertDialog( title: Text( label, - style: const TextStyle(fontFamily: 'monospace', fontSize: 16), + style: textTheme.bodyEmphasis.copyWith( + fontFamily: 'monospace', + ), ), content: SingleChildScrollView( child: ColorPicker( diff --git a/apps/design_system_gallery/lib/widgets/theme_studio/mode_button.dart b/apps/design_system_gallery/lib/widgets/theme_studio/mode_button.dart index 15a4af4..69baeba 100644 --- a/apps/design_system_gallery/lib/widgets/theme_studio/mode_button.dart +++ b/apps/design_system_gallery/lib/widgets/theme_studio/mode_button.dart @@ -8,34 +8,35 @@ class ThemeStudioModeButton extends StatelessWidget { required this.label, required this.icon, required this.isSelected, - required this.colorScheme, - required this.textTheme, required this.onTap, }); final String label; final IconData icon; final bool isSelected; - final StreamColorScheme colorScheme; - final StreamTextTheme textTheme; final VoidCallback onTap; @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + return Material( color: isSelected ? colorScheme.accentPrimary.withValues(alpha: 0.1) : colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), child: Container( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), + padding: EdgeInsets.symmetric(vertical: spacing.sm + spacing.xxs, horizontal: spacing.sm), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), ), foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), border: Border.all( color: isSelected ? colorScheme.accentPrimary : colorScheme.borderSurfaceSubtle, width: isSelected ? 2 : 1, @@ -48,7 +49,7 @@ class ThemeStudioModeButton extends StatelessWidget { color: isSelected ? colorScheme.accentPrimary : colorScheme.textTertiary, size: 20, ), - const SizedBox(height: 4), + SizedBox(height: spacing.xs), Text( label, style: textTheme.captionEmphasis.copyWith( diff --git a/apps/design_system_gallery/lib/widgets/theme_studio/section_card.dart b/apps/design_system_gallery/lib/widgets/theme_studio/section_card.dart index 5181f20..306793c 100644 --- a/apps/design_system_gallery/lib/widgets/theme_studio/section_card.dart +++ b/apps/design_system_gallery/lib/widgets/theme_studio/section_card.dart @@ -7,14 +7,12 @@ import 'package:stream_core_flutter/stream_core_flutter.dart'; class SectionCard extends StatelessWidget { const SectionCard({ super.key, - required this.colorScheme, required this.title, required this.subtitle, required this.icon, required this.child, }); - final StreamColorScheme colorScheme; final String title; final String subtitle; final IconData icon; @@ -22,15 +20,20 @@ class SectionCard extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + return Container( - padding: const EdgeInsets.all(12), + padding: EdgeInsets.all(spacing.sm), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.all(radius.md), ), foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.all(radius.md), border: Border.all(color: colorScheme.borderSurfaceSubtle), ), child: Column( @@ -39,34 +42,31 @@ class SectionCard extends StatelessWidget { Row( children: [ Icon(icon, color: colorScheme.textTertiary, size: 14), - const SizedBox(width: 6), + SizedBox(width: spacing.xs + spacing.xxs), Text( title, - style: TextStyle( + style: textTheme.captionEmphasis.copyWith( color: colorScheme.textPrimary, - fontWeight: FontWeight.w600, - fontSize: 12, ), ), - const SizedBox(width: 6), + SizedBox(width: spacing.xs + spacing.xxs), Container( - padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), + padding: EdgeInsets.symmetric(horizontal: spacing.xs, vertical: 1), decoration: BoxDecoration( color: colorScheme.backgroundSurfaceSubtle, - borderRadius: BorderRadius.circular(4), + borderRadius: BorderRadius.all(radius.xs), ), child: Text( subtitle, - style: TextStyle( + style: textTheme.metadataDefault.copyWith( color: colorScheme.textTertiary, - fontSize: 9, fontFamily: 'monospace', ), ), ), ], ), - const SizedBox(height: 10), + SizedBox(height: spacing.sm + spacing.xxs), child, ], ), diff --git a/apps/design_system_gallery/lib/widgets/theme_studio/theme_customization_panel.dart b/apps/design_system_gallery/lib/widgets/theme_studio/theme_customization_panel.dart index 4c74ec1..b397956 100644 --- a/apps/design_system_gallery/lib/widgets/theme_studio/theme_customization_panel.dart +++ b/apps/design_system_gallery/lib/widgets/theme_studio/theme_customization_panel.dart @@ -1,6 +1,7 @@ import 'dart:math' show Random; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:stream_core_flutter/stream_core_flutter.dart'; import '../../config/theme_configuration.dart'; @@ -35,117 +36,88 @@ StreamAvatarColorPair _generateRandomAvatarPair({required bool isDark}) { /// A panel widget for customizing the Stream theme. /// /// Organized into sections matching [StreamColorScheme] structure. -class ThemeCustomizationPanel extends StatefulWidget { - const ThemeCustomizationPanel({ - super.key, - required this.configuration, - }); - - final ThemeConfiguration configuration; - - @override - State createState() => _ThemeCustomizationPanelState(); -} - -class _ThemeCustomizationPanelState extends State { - final _scrollController = ScrollController(); - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } +class ThemeCustomizationPanel extends StatelessWidget { + const ThemeCustomizationPanel({super.key}); @override Widget build(BuildContext context) { - return ListenableBuilder( - listenable: widget.configuration, - builder: (context, _) { - final streamTheme = widget.configuration.themeData; - final colorScheme = streamTheme.colorScheme; - final textTheme = streamTheme.textTheme; - final boxShadow = streamTheme.boxShadow; - final materialTheme = widget.configuration.buildMaterialTheme(); + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; - // Wrap with Theme to apply Stream theming to all widgets - return Theme( - data: materialTheme, - child: DecoratedBox( - decoration: BoxDecoration( - color: colorScheme.backgroundApp, - border: Border( - left: BorderSide(color: colorScheme.borderSurfaceSubtle), - ), - ), - child: Column( - children: [ - _buildHeader(colorScheme, textTheme), - Expanded( - child: Scrollbar( - controller: _scrollController, - thumbVisibility: true, - child: SingleChildScrollView( - controller: _scrollController, - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildAppearanceSection(colorScheme, textTheme), - const SizedBox(height: 16), - _buildBrandSection(colorScheme, boxShadow), - const SizedBox(height: 16), - _buildAccentColorsSection(colorScheme, boxShadow), - const SizedBox(height: 16), - _buildTextColorsSection(colorScheme, boxShadow), - const SizedBox(height: 16), - _buildBackgroundColorsSection(colorScheme, boxShadow), - const SizedBox(height: 16), - _buildBorderCoreSection(colorScheme, boxShadow), - const SizedBox(height: 16), - _buildBorderUtilitySection(colorScheme, boxShadow), - const SizedBox(height: 16), - _buildStateColorsSection(colorScheme, boxShadow), - const SizedBox(height: 16), - _buildSystemColorsSection(colorScheme, boxShadow), - const SizedBox(height: 16), - _buildAvatarPaletteSection(colorScheme), - const SizedBox(height: 16), - ], - ), - ), - ), + return Container( + decoration: BoxDecoration( + color: colorScheme.backgroundApp, + ), + foregroundDecoration: BoxDecoration( + border: .symmetric( + vertical: .new(color: colorScheme.borderSurfaceSubtle), + ), + ), + child: Column( + children: [ + _buildHeader(context), + Expanded( + child: Scrollbar( + thumbVisibility: true, + child: SingleChildScrollView( + padding: EdgeInsets.all(spacing.md), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildAppearanceSection(context), + SizedBox(height: spacing.md), + _buildBrandSection(context), + SizedBox(height: spacing.md), + _buildAccentColorsSection(context), + SizedBox(height: spacing.md), + _buildTextColorsSection(context), + SizedBox(height: spacing.md), + _buildBackgroundColorsSection(context), + SizedBox(height: spacing.md), + _buildBorderCoreSection(context), + SizedBox(height: spacing.md), + _buildBorderUtilitySection(context), + SizedBox(height: spacing.md), + _buildStateColorsSection(context), + SizedBox(height: spacing.md), + _buildSystemColorsSection(context), + SizedBox(height: spacing.md), + _buildAvatarPaletteSection(context), + SizedBox(height: spacing.md), + ], ), - ], + ), ), ), - ); - }, + ], + ), ); } - Widget _buildHeader( - StreamColorScheme colorScheme, - StreamTextTheme textTheme, - ) { + Widget _buildHeader(BuildContext context) { + final config = context.read(); + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + return Container( - padding: const EdgeInsets.all(16), + padding: EdgeInsets.all(spacing.md), decoration: BoxDecoration( color: colorScheme.backgroundSurface, - border: Border( - bottom: BorderSide(color: colorScheme.borderSurfaceSubtle), - ), + border: Border(bottom: .new(color: colorScheme.borderSurfaceSubtle)), ), child: Row( children: [ Container( - padding: const EdgeInsets.all(8), + padding: EdgeInsets.all(spacing.sm), decoration: BoxDecoration( color: colorScheme.accentPrimary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), ), child: Icon(Icons.tune, color: colorScheme.accentPrimary, size: 20), ), - const SizedBox(width: 12), + SizedBox(width: spacing.sm), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -171,12 +143,12 @@ class _ThemeCustomizationPanelState extends State { message: 'Reset to defaults', child: Material( color: Colors.transparent, - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.all(radius.sm), child: InkWell( - onTap: widget.configuration.resetToDefaults, - borderRadius: BorderRadius.circular(6), + onTap: config.resetToDefaults, + borderRadius: BorderRadius.all(radius.sm), child: Padding( - padding: const EdgeInsets.all(8), + padding: EdgeInsets.all(spacing.sm), child: Icon( Icons.restart_alt, color: colorScheme.textTertiary, @@ -191,12 +163,11 @@ class _ThemeCustomizationPanelState extends State { ); } - Widget _buildAppearanceSection( - StreamColorScheme colorScheme, - StreamTextTheme textTheme, - ) { + Widget _buildAppearanceSection(BuildContext context) { + final config = context.read(); + final spacing = context.streamSpacing; + return SectionCard( - colorScheme: colorScheme, title: 'Appearance', subtitle: 'brightness', icon: Icons.brightness_6, @@ -206,21 +177,17 @@ class _ThemeCustomizationPanelState extends State { child: ThemeStudioModeButton( label: 'Light', icon: Icons.light_mode, - isSelected: widget.configuration.brightness == Brightness.light, - colorScheme: colorScheme, - textTheme: textTheme, - onTap: () => widget.configuration.setBrightness(Brightness.light), + isSelected: config.brightness == Brightness.light, + onTap: () => config.setBrightness(Brightness.light), ), ), - const SizedBox(width: 12), + SizedBox(width: spacing.sm), Expanded( child: ThemeStudioModeButton( label: 'Dark', icon: Icons.dark_mode, - isSelected: widget.configuration.brightness == Brightness.dark, - colorScheme: colorScheme, - textTheme: textTheme, - onTap: () => widget.configuration.setBrightness(Brightness.dark), + isSelected: config.brightness == Brightness.dark, + onTap: () => config.setBrightness(Brightness.dark), ), ), ], @@ -228,34 +195,23 @@ class _ThemeCustomizationPanelState extends State { ); } - Widget _buildBrandSection( - StreamColorScheme colorScheme, - StreamBoxShadow boxShadow, - ) { - final config = widget.configuration; - + Widget _buildBrandSection(BuildContext context) { + final config = context.read(); return SectionCard( - colorScheme: colorScheme, title: 'Brand Color', subtitle: 'brand', icon: Icons.branding_watermark, child: ColorPickerTile( label: 'brandPrimary', color: config.brandPrimaryColor, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBrandPrimaryColor, ), ); } - Widget _buildAccentColorsSection( - StreamColorScheme colorScheme, - StreamBoxShadow boxShadow, - ) { - final config = widget.configuration; + Widget _buildAccentColorsSection(BuildContext context) { + final config = context.read(); return SectionCard( - colorScheme: colorScheme, title: 'Accent Colors', subtitle: 'accent*', icon: Icons.color_lens, @@ -264,36 +220,26 @@ class _ThemeCustomizationPanelState extends State { ColorPickerTile( label: 'accentPrimary', color: config.accentPrimary, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setAccentPrimary, ), ColorPickerTile( label: 'accentSuccess', color: config.accentSuccess, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setAccentSuccess, ), ColorPickerTile( label: 'accentWarning', color: config.accentWarning, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setAccentWarning, ), ColorPickerTile( label: 'accentError', color: config.accentError, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setAccentError, ), ColorPickerTile( label: 'accentNeutral', color: config.accentNeutral, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setAccentNeutral, ), ], @@ -301,13 +247,9 @@ class _ThemeCustomizationPanelState extends State { ); } - Widget _buildTextColorsSection( - StreamColorScheme colorScheme, - StreamBoxShadow boxShadow, - ) { - final config = widget.configuration; + Widget _buildTextColorsSection(BuildContext context) { + final config = context.read(); return SectionCard( - colorScheme: colorScheme, title: 'Text Colors', subtitle: 'text*', icon: Icons.format_color_text, @@ -316,50 +258,36 @@ class _ThemeCustomizationPanelState extends State { ColorPickerTile( label: 'textPrimary', color: config.textPrimary, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setTextPrimary, ), ColorPickerTile( label: 'textSecondary', color: config.textSecondary, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setTextSecondary, ), ColorPickerTile( label: 'textTertiary', color: config.textTertiary, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setTextTertiary, ), ColorPickerTile( label: 'textDisabled', color: config.textDisabled, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setTextDisabled, ), ColorPickerTile( label: 'textInverse', color: config.textInverse, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setTextInverse, ), ColorPickerTile( label: 'textLink', color: config.textLink, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setTextLink, ), ColorPickerTile( label: 'textOnAccent', color: config.textOnAccent, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setTextOnAccent, ), ], @@ -367,13 +295,9 @@ class _ThemeCustomizationPanelState extends State { ); } - Widget _buildBackgroundColorsSection( - StreamColorScheme colorScheme, - StreamBoxShadow boxShadow, - ) { - final config = widget.configuration; + Widget _buildBackgroundColorsSection(BuildContext context) { + final config = context.read(); return SectionCard( - colorScheme: colorScheme, title: 'Background Colors', subtitle: 'background*', icon: Icons.format_paint, @@ -382,36 +306,26 @@ class _ThemeCustomizationPanelState extends State { ColorPickerTile( label: 'backgroundApp', color: config.backgroundApp, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBackgroundApp, ), ColorPickerTile( label: 'backgroundSurface', color: config.backgroundSurface, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBackgroundSurface, ), ColorPickerTile( label: 'backgroundSurfaceSubtle', color: config.backgroundSurfaceSubtle, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBackgroundSurfaceSubtle, ), ColorPickerTile( label: 'backgroundSurfaceStrong', color: config.backgroundSurfaceStrong, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBackgroundSurfaceStrong, ), ColorPickerTile( label: 'backgroundOverlay', color: config.backgroundOverlay, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBackgroundOverlay, ), ], @@ -419,13 +333,9 @@ class _ThemeCustomizationPanelState extends State { ); } - Widget _buildBorderCoreSection( - StreamColorScheme colorScheme, - StreamBoxShadow boxShadow, - ) { - final config = widget.configuration; + Widget _buildBorderCoreSection(BuildContext context) { + final config = context.read(); return SectionCard( - colorScheme: colorScheme, title: 'Border Colors - Core', subtitle: 'border*', icon: Icons.border_all, @@ -434,50 +344,36 @@ class _ThemeCustomizationPanelState extends State { ColorPickerTile( label: 'borderSurface', color: config.borderSurface, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderSurface, ), ColorPickerTile( label: 'borderSurfaceSubtle', color: config.borderSurfaceSubtle, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderSurfaceSubtle, ), ColorPickerTile( label: 'borderSurfaceStrong', color: config.borderSurfaceStrong, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderSurfaceStrong, ), ColorPickerTile( label: 'borderOnDark', color: config.borderOnDark, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderOnDark, ), ColorPickerTile( label: 'borderOnAccent', color: config.borderOnAccent, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderOnAccent, ), ColorPickerTile( label: 'borderSubtle', color: config.borderSubtle, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderSubtle, ), ColorPickerTile( label: 'borderImage', color: config.borderImage, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderImage, ), ], @@ -485,13 +381,9 @@ class _ThemeCustomizationPanelState extends State { ); } - Widget _buildBorderUtilitySection( - StreamColorScheme colorScheme, - StreamBoxShadow boxShadow, - ) { - final config = widget.configuration; + Widget _buildBorderUtilitySection(BuildContext context) { + final config = context.read(); return SectionCard( - colorScheme: colorScheme, title: 'Border Colors - Utility', subtitle: 'border*', icon: Icons.border_style, @@ -500,43 +392,31 @@ class _ThemeCustomizationPanelState extends State { ColorPickerTile( label: 'borderFocus', color: config.borderFocus, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderFocus, ), ColorPickerTile( label: 'borderDisabled', color: config.borderDisabled, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderDisabled, ), ColorPickerTile( label: 'borderError', color: config.borderError, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderError, ), ColorPickerTile( label: 'borderWarning', color: config.borderWarning, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderWarning, ), ColorPickerTile( label: 'borderSuccess', color: config.borderSuccess, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderSuccess, ), ColorPickerTile( label: 'borderSelected', color: config.borderSelected, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setBorderSelected, ), ], @@ -544,13 +424,9 @@ class _ThemeCustomizationPanelState extends State { ); } - Widget _buildStateColorsSection( - StreamColorScheme colorScheme, - StreamBoxShadow boxShadow, - ) { - final config = widget.configuration; + Widget _buildStateColorsSection(BuildContext context) { + final config = context.read(); return SectionCard( - colorScheme: colorScheme, title: 'State Colors', subtitle: 'state*', icon: Icons.touch_app, @@ -559,36 +435,26 @@ class _ThemeCustomizationPanelState extends State { ColorPickerTile( label: 'stateHover', color: config.stateHover, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setStateHover, ), ColorPickerTile( label: 'statePressed', color: config.statePressed, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setStatePressed, ), ColorPickerTile( label: 'stateSelected', color: config.stateSelected, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setStateSelected, ), ColorPickerTile( label: 'stateFocused', color: config.stateFocused, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setStateFocused, ), ColorPickerTile( label: 'stateDisabled', color: config.stateDisabled, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setStateDisabled, ), ], @@ -596,13 +462,9 @@ class _ThemeCustomizationPanelState extends State { ); } - Widget _buildSystemColorsSection( - StreamColorScheme colorScheme, - StreamBoxShadow boxShadow, - ) { - final config = widget.configuration; + Widget _buildSystemColorsSection(BuildContext context) { + final config = context.read(); return SectionCard( - colorScheme: colorScheme, title: 'System Colors', subtitle: 'system*', icon: Icons.settings_system_daydream, @@ -611,15 +473,11 @@ class _ThemeCustomizationPanelState extends State { ColorPickerTile( label: 'systemText', color: config.systemText, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setSystemText, ), ColorPickerTile( label: 'systemScrollbar', color: config.systemScrollbar, - colorScheme: colorScheme, - boxShadow: boxShadow, onColorChanged: config.setSystemScrollbar, ), ], @@ -627,12 +485,12 @@ class _ThemeCustomizationPanelState extends State { ); } - Widget _buildAvatarPaletteSection(StreamColorScheme colorScheme) { - final config = widget.configuration; + Widget _buildAvatarPaletteSection(BuildContext context) { + final config = context.read(); final palette = config.avatarPalette; + final spacing = context.streamSpacing; return SectionCard( - colorScheme: colorScheme, title: 'Avatar Palette', subtitle: 'avatarPalette', icon: Icons.palette, @@ -643,7 +501,6 @@ class _ThemeCustomizationPanelState extends State { return AvatarColorPairTile( index: index, pair: pair, - colorScheme: colorScheme, onBackgroundChanged: (color) { config.updateAvatarPaletteAt( index, @@ -665,9 +522,8 @@ class _ThemeCustomizationPanelState extends State { onRemove: palette.length > 1 ? () => config.removeAvatarPaletteAt(index) : null, ); }), - const SizedBox(height: 8), + SizedBox(height: spacing.sm), AddPaletteButton( - colorScheme: colorScheme, onTap: () { final isDark = config.brightness == Brightness.dark; config.addAvatarPaletteEntry(_generateRandomAvatarPair(isDark: isDark)); diff --git a/apps/design_system_gallery/lib/widgets/toolbar/device_selector.dart b/apps/design_system_gallery/lib/widgets/toolbar/device_selector.dart index 6cc5caf..290c485 100644 --- a/apps/design_system_gallery/lib/widgets/toolbar/device_selector.dart +++ b/apps/design_system_gallery/lib/widgets/toolbar/device_selector.dart @@ -8,28 +8,29 @@ class DeviceSelector extends StatelessWidget { super.key, required this.selectedDevice, required this.devices, - required this.colorScheme, - required this.textTheme, required this.onDeviceChanged, }); final DeviceInfo selectedDevice; final List devices; - final StreamColorScheme colorScheme; - final StreamTextTheme textTheme; final ValueChanged onDeviceChanged; @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + return Container( - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: EdgeInsets.symmetric(horizontal: spacing.sm), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), ), foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), border: Border.all(color: colorScheme.borderSurfaceSubtle), ), child: DropdownButtonHideUnderline( @@ -59,8 +60,8 @@ class DeviceSelector extends StatelessWidget { size: 14, color: colorScheme.textTertiary, ), - const SizedBox(width: 8), - Text(device.name, style: const TextStyle(fontSize: 13)), + SizedBox(width: spacing.sm), + Text(device.name, style: textTheme.captionDefault), ], ), ); diff --git a/apps/design_system_gallery/lib/widgets/toolbar/text_scale_selector.dart b/apps/design_system_gallery/lib/widgets/toolbar/text_scale_selector.dart index 03625dd..7ceb4b3 100644 --- a/apps/design_system_gallery/lib/widgets/toolbar/text_scale_selector.dart +++ b/apps/design_system_gallery/lib/widgets/toolbar/text_scale_selector.dart @@ -7,28 +7,29 @@ class TextScaleSelector extends StatelessWidget { super.key, required this.value, required this.options, - required this.colorScheme, - required this.textTheme, required this.onChanged, }); final double value; final List options; - final StreamColorScheme colorScheme; - final StreamTextTheme textTheme; final ValueChanged onChanged; @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + return Container( - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: EdgeInsets.symmetric(horizontal: spacing.sm), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), ), foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), border: Border.all(color: colorScheme.borderSurfaceSubtle), ), child: DropdownButtonHideUnderline( @@ -54,10 +55,10 @@ class TextScaleSelector extends StatelessWidget { size: 14, color: colorScheme.textTertiary, ), - const SizedBox(width: 8), + SizedBox(width: spacing.sm), Text( '${(scale * 100).toInt()}%', - style: const TextStyle(fontSize: 13), + style: textTheme.captionDefault, ), ], ), diff --git a/apps/design_system_gallery/lib/widgets/toolbar/theme_mode_toggle.dart b/apps/design_system_gallery/lib/widgets/toolbar/theme_mode_toggle.dart index 295529d..37d503f 100644 --- a/apps/design_system_gallery/lib/widgets/toolbar/theme_mode_toggle.dart +++ b/apps/design_system_gallery/lib/widgets/toolbar/theme_mode_toggle.dart @@ -6,26 +6,27 @@ class ThemeModeToggle extends StatelessWidget { const ThemeModeToggle({ super.key, required this.isDark, - required this.colorScheme, required this.onLightTap, required this.onDarkTap, }); final bool isDark; - final StreamColorScheme colorScheme; final VoidCallback onLightTap; final VoidCallback onDarkTap; @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final radius = context.streamRadius; + return Container( clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), ), foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), border: Border.all(color: colorScheme.borderSurfaceSubtle), ), child: Row( @@ -34,9 +35,8 @@ class ThemeModeToggle extends StatelessWidget { _ModeButton( icon: Icons.light_mode, isSelected: !isDark, - colorScheme: colorScheme, onTap: onLightTap, - borderRadius: const BorderRadius.horizontal(left: Radius.circular(7)), + borderRadius: BorderRadius.horizontal(left: Radius.circular(radius.md.x - 1)), ), ColoredBox( color: colorScheme.borderSurfaceSubtle, @@ -45,9 +45,8 @@ class ThemeModeToggle extends StatelessWidget { _ModeButton( icon: Icons.dark_mode, isSelected: isDark, - colorScheme: colorScheme, onTap: onDarkTap, - borderRadius: const BorderRadius.horizontal(right: Radius.circular(7)), + borderRadius: BorderRadius.horizontal(right: Radius.circular(radius.md.x - 1)), ), ], ), @@ -59,19 +58,20 @@ class _ModeButton extends StatelessWidget { const _ModeButton({ required this.icon, required this.isSelected, - required this.colorScheme, required this.onTap, required this.borderRadius, }); final IconData icon; final bool isSelected; - final StreamColorScheme colorScheme; final VoidCallback onTap; final BorderRadius borderRadius; @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; + return Material( color: isSelected ? colorScheme.accentPrimary.withValues(alpha: 0.1) : Colors.transparent, borderRadius: borderRadius, @@ -79,7 +79,7 @@ class _ModeButton extends StatelessWidget { onTap: onTap, borderRadius: borderRadius, child: Padding( - padding: const EdgeInsets.all(10), + padding: EdgeInsets.all(spacing.sm + spacing.xxs), child: Icon( icon, size: 18, diff --git a/apps/design_system_gallery/lib/widgets/toolbar/toolbar.dart b/apps/design_system_gallery/lib/widgets/toolbar/toolbar.dart index 0f6d7c1..5eebce1 100644 --- a/apps/design_system_gallery/lib/widgets/toolbar/toolbar.dart +++ b/apps/design_system_gallery/lib/widgets/toolbar/toolbar.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:stream_core_flutter/stream_core_flutter.dart'; +import 'package:svg_icon_widget/svg_icon_widget.dart'; import '../../config/preview_configuration.dart'; import '../../config/theme_configuration.dart'; +import '../../core/stream_icons.dart'; import 'device_selector.dart'; import 'text_scale_selector.dart'; import 'theme_mode_toggle.dart'; @@ -24,17 +26,16 @@ class GalleryToolbar extends StatelessWidget { @override Widget build(BuildContext context) { - final themeConfig = context.watch(); + // Use read (not watch) - rebuilds come from Consumer in gallery_app.dart + final themeConfig = context.read(); final previewConfig = context.watch(); - final streamTheme = themeConfig.themeData; - final colorScheme = streamTheme.colorScheme; - final textTheme = streamTheme.textTheme; - final boxShadow = streamTheme.boxShadow; - final isDark = themeConfig.brightness == Brightness.dark; + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; + final isDark = Theme.of(context).brightness == Brightness.dark; return Container( height: 64, - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: EdgeInsets.symmetric(horizontal: spacing.md), decoration: BoxDecoration( color: colorScheme.backgroundSurface, border: Border( @@ -44,12 +45,8 @@ class GalleryToolbar extends StatelessWidget { child: Row( children: [ // Stream Logo and title - _StreamBranding( - colorScheme: colorScheme, - textTheme: textTheme, - boxShadow: boxShadow, - ), - const SizedBox(width: 24), + const _StreamBranding(), + SizedBox(width: spacing.lg), // Toolbar controls - wrapped in Expanded to prevent overflow Expanded( @@ -63,29 +60,24 @@ class GalleryToolbar extends StatelessWidget { icon: previewConfig.showDeviceFrame ? Icons.devices : Icons.phone_android, tooltip: 'Device Frame', isActive: previewConfig.showDeviceFrame, - colorScheme: colorScheme, onTap: previewConfig.toggleDeviceFrame, ), - const SizedBox(width: 8), + SizedBox(width: spacing.sm), // Device selector if (previewConfig.showDeviceFrame) ...[ DeviceSelector( selectedDevice: previewConfig.selectedDevice, devices: PreviewConfiguration.deviceOptions, - colorScheme: colorScheme, - textTheme: textTheme, onDeviceChanged: previewConfig.setDevice, ), - const SizedBox(width: 8), + SizedBox(width: spacing.sm), ], // Text scale selector TextScaleSelector( value: previewConfig.textScale, options: PreviewConfiguration.textScaleOptions, - colorScheme: colorScheme, - textTheme: textTheme, onChanged: previewConfig.setTextScale, ), ], @@ -93,23 +85,21 @@ class GalleryToolbar extends StatelessWidget { ), ), - const SizedBox(width: 16), + SizedBox(width: spacing.md), // Theme mode toggle ThemeModeToggle( isDark: isDark, - colorScheme: colorScheme, onLightTap: () => themeConfig.setBrightness(Brightness.light), onDarkTap: () => themeConfig.setBrightness(Brightness.dark), ), - const SizedBox(width: 8), + SizedBox(width: spacing.sm), // Theme panel toggle ToolbarButton( icon: showThemePanel ? Icons.palette : Icons.palette_outlined, tooltip: 'Theme Studio', isActive: showThemePanel, - colorScheme: colorScheme, onTap: onToggleThemePanel, ), ], @@ -120,51 +110,20 @@ class GalleryToolbar extends StatelessWidget { /// Stream branding logo and title. class _StreamBranding extends StatelessWidget { - const _StreamBranding({ - required this.colorScheme, - required this.textTheme, - required this.boxShadow, - }); - - final StreamColorScheme colorScheme; - final StreamTextTheme textTheme; - final StreamBoxShadow boxShadow; + const _StreamBranding(); @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + return Row( mainAxisSize: MainAxisSize.min, children: [ // Stream Logo - Container( - width: 40, - height: 40, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - colorScheme.accentPrimary, - colorScheme.accentPrimary.withValues(alpha: 0.7), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(10), - boxShadow: boxShadow.elevation2, - ), - child: Center( - child: Text( - 'S', - style: TextStyle( - color: colorScheme.textOnAccent, - fontSize: 22, - fontWeight: FontWeight.w700, - letterSpacing: -0.5, - ), - ), - ), - ), - const SizedBox(width: 14), + const SvgIcon(StreamIcons.logo, size: 40), + SizedBox(width: spacing.sm + spacing.xxs), Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/apps/design_system_gallery/lib/widgets/toolbar/toolbar_button.dart b/apps/design_system_gallery/lib/widgets/toolbar/toolbar_button.dart index 81ad31c..e3ae05a 100644 --- a/apps/design_system_gallery/lib/widgets/toolbar/toolbar_button.dart +++ b/apps/design_system_gallery/lib/widgets/toolbar/toolbar_button.dart @@ -8,34 +8,36 @@ class ToolbarButton extends StatelessWidget { required this.icon, required this.tooltip, required this.isActive, - required this.colorScheme, required this.onTap, }); final IconData icon; final String tooltip; final bool isActive; - final StreamColorScheme colorScheme; final VoidCallback onTap; @override Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + return Tooltip( message: tooltip, child: Material( color: isActive ? colorScheme.accentPrimary.withValues(alpha: 0.1) : colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), child: Container( - padding: const EdgeInsets.all(10), + padding: EdgeInsets.all(spacing.sm + spacing.xxs), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), ), foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.all(radius.md), border: Border.all( color: isActive ? colorScheme.accentPrimary : colorScheme.borderSurfaceSubtle, ), diff --git a/apps/design_system_gallery/pubspec.yaml b/apps/design_system_gallery/pubspec.yaml index 65c02a4..d584373 100644 --- a/apps/design_system_gallery/pubspec.yaml +++ b/apps/design_system_gallery/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: provider: ^6.1.5+1 stream_core_flutter: path: ../../packages/stream_core_flutter + svg_icon_widget: ^0.0.1+1 widgetbook: ^3.20.2 widgetbook_annotation: ^3.9.0 @@ -25,4 +26,6 @@ dev_dependencies: widgetbook_generator: ^3.20.1 flutter: - uses-material-design: true \ No newline at end of file + uses-material-design: true + assets: + - assets/ \ No newline at end of file diff --git a/melos.yaml b/melos.yaml index 3f7da17..11994af 100644 --- a/melos.yaml +++ b/melos.yaml @@ -31,6 +31,7 @@ command: mime: ^2.0.0 rxdart: ^0.28.0 stream_core: ^0.4.0 + svg_icon_widget: ^0.0.1+1 synchronized: ^3.3.0 theme_extensions_builder_annotation: ^7.1.0 uuid: ^4.5.1