material_glimmer_ui 0.7.0 copy "material_glimmer_ui: ^0.7.0" to clipboard
material_glimmer_ui: ^0.7.0 copied to clipboard

A Flutter UI kit of glass surfaces and lit edges. An app widget, a theme, a full token set and a widget library. Inspired by Jetpack Compose Glimmer and Material Design.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:material_glimmer_ui/material_glimmer_ui.dart';

void main() => runApp(const MaterialGlimmerGallery());

/// The gallery app.
class MaterialGlimmerGallery extends StatefulWidget {
  /// Creates the gallery.
  const MaterialGlimmerGallery({super.key});

  @override
  State<MaterialGlimmerGallery> createState() => _MaterialGlimmerGalleryState();
}

class _MaterialGlimmerGalleryState extends State<MaterialGlimmerGallery> {
  var _mode = ThemeMode.dark;

  @override
  Widget build(BuildContext context) {
    return MaterialGlimmerApp(
      title: 'Material Glimmer UI',
      debugShowCheckedModeBanner: false,
      // The package ships no assets, so the typeface is the app's to choose.
      // Every style in the kit flows from the theme, so setting it once here
      // covers every piece of text in the gallery.
      fontFamily: 'Google Sans Flex',
      themeMode: _mode,
      home: _GalleryHome(
        mode: _mode,
        onModeChanged: (mode) => setState(() => _mode = mode),
      ),
    );
  }
}

class _GalleryHome extends StatefulWidget {
  const _GalleryHome({
    required this.mode,
    required this.onModeChanged,
  });

  final ThemeMode mode;
  final ValueChanged<ThemeMode> onModeChanged;

  @override
  State<_GalleryHome> createState() => _GalleryHomeState();
}

class _GalleryHomeState extends State<_GalleryHome> {
  var _tab = 0;

  @override
  Widget build(BuildContext context) {
    final pages = [
      const _ComponentsPage(),
      const _FoundationsPage(),
    ];

    return GlimmerScaffold(
      backdrop: const GlimmerBackdrop(),
      title: 'Material Glimmer UI',
      action: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          GlimmerIconButton(
            icon: widget.mode == ThemeMode.dark
                ? Icons.dark_mode_outlined
                : Icons.light_mode_outlined,
            tooltip: widget.mode == ThemeMode.dark
                ? 'Switch to light'
                : 'Switch to dark',
            onPressed: () => widget.onModeChanged(
              widget.mode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark,
            ),
          ),
          SizedBox(width: GlimmerTheme.of(context).spacing.small),
          GlimmerIconButton(
            icon: Icons.auto_awesome,
            tooltip: 'About',
            onPressed: () => showGlimmerDialog<void>(
              context: context,
              builder: (context) => const _AboutSheet(),
            ),
          ),
        ],
      ),
      body: IndexedStack(index: _tab, children: pages),
      floatingAction: GlimmerFab(
        icon: Icons.add,
        label: _tab == 0 ? 'Add an item' : null,
        tooltip: 'Add an item',
        onPressed: () => showGlimmerSnackbar(
          context,
          message: 'Nothing to add in a gallery',
          icon: Icons.info_outline,
          bottomInset: _navigationStripHeight,
        ),
      ),
      selectedIndex: _tab,
      onNavigationChanged: (value) => setState(() => _tab = value),
      navigationItems: const [
        GlimmerNavigationItem(
            label: 'Components', icon: Icons.widgets_outlined),
        GlimmerNavigationItem(label: 'Foundations', icon: Icons.tune),
      ],
    );
  }
}

class _ComponentsPage extends StatefulWidget {
  const _ComponentsPage();

  @override
  State<_ComponentsPage> createState() => _ComponentsPageState();
}

/// Roughly the height of the gallery's own navigation strip, so a message
/// clears it instead of landing on top.
const _navigationStripHeight = 76.0;

class _ComponentsPageState extends State<_ComponentsPage> {
  final _bought = <String>{};
  var _saved = false;
  var _stackIndex = 0;
  var _refreshed = false;
  var _muted = false;
  var _listening = true;
  var _ambient = false;
  var _progress = 0.4;
  var _digest = true;
  var _roast = 'Medium';
  var _filter = 0;
  var _query = '';
  final _tags = <String>{'Open now'};
  var _loading = true;

  static const _pagerPages = <(String, String, IconData)>[
    ('Museu do Café', 'Santos, open until five', Icons.museum_outlined),
    ('Pinacoteca', 'Luz, new exhibition', Icons.palette_outlined),
    ('Mercado Municipal', 'Centro, mortadella sandwich', Icons.storefront),
  ];

  static const _groceries = [
    'Pão de queijo',
    'Jabuticaba',
    'Papaya',
    'Kale',
    'Coffee',
  ];

  Future<void> _openDialog(BuildContext context) async {
    final leaving = await showGlimmerDialog<bool>(
      context: context,
      builder: (context) => GlimmerDialog(
        icon: Icons.storefront,
        title: 'Leave the queue?',
        content: 'You are third in line at the Mercado Municipal.',
        actions: [
          GlimmerButton(
            label: 'Stay',
            onPressed: () => Navigator.of(context).pop(false),
          ),
          GlimmerButton(
            label: 'Leave',
            prominent: true,
            onPressed: () => Navigator.of(context).pop(true),
          ),
        ],
      ),
    );
    if (leaving == true && context.mounted) {
      showGlimmerSnackbar(
        context,
        message: 'You left the queue',
        bottomInset: _navigationStripHeight,
      );
    }
  }

  Future<void> _openSheet(BuildContext context) async {
    final choice = await showGlimmerBottomSheet<String>(
      context: context,
      builder: (context) => GlimmerBottomSheet(
        title: 'Sort the market list',
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            for (final option in const ['By aisle', 'By price', 'Alphabetical'])
              Padding(
                padding: const EdgeInsets.only(bottom: 8),
                child: GlimmerListItem(
                  label: option,
                  leadingIcon: Icons.sort,
                  onTap: () => Navigator.of(context).pop(option),
                ),
              ),
            // Raised from inside the sheet, so the sheet is still there when
            // the message lands. That is what the bottom inset is for.
            GlimmerListItem(
              label: 'Save this order',
              supportingLabel: 'The sheet stays open',
              leadingIcon: Icons.bookmark_add_outlined,
              onTap: () => showGlimmerSnackbar(
                context,
                message: 'Added to the Saturday market list',
                icon: Icons.check_circle_outline,
                actionLabel: 'Undo',
                onAction: () {},
              ),
            ),
          ],
        ),
      ),
    );
    if (choice != null && context.mounted) {
      showGlimmerSnackbar(
        context,
        message: 'Sorted $choice',
        bottomInset: _navigationStripHeight,
      );
    }
  }

  Future<void> _openMenu(BuildContext context) async {
    final choice = await showGlimmerMenu<String>(
      context: context,
      items: const [
        GlimmerMenuItem(
          label: 'Share the list',
          value: 'share',
          icon: Icons.ios_share,
        ),
        GlimmerMenuItem(
          label: 'Duplicate',
          value: 'duplicate',
          icon: Icons.copy_all_outlined,
        ),
        GlimmerMenuItem(
          label: 'Not available offline',
          value: 'offline',
          icon: Icons.cloud_off,
          enabled: false,
        ),
        GlimmerMenuItem(
          label: 'Delete the list',
          value: 'delete',
          icon: Icons.delete_outline,
          destructive: true,
        ),
      ],
    );
    if (choice != null && context.mounted) {
      showGlimmerSnackbar(
        context,
        message: 'Chose $choice',
        bottomInset: _navigationStripHeight,
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    final tokens = GlimmerTheme.of(context);
    final type = tokens.typography;
    final spacing = tokens.spacing;

    // Pull the page down past the top: the edge lights, holds and breathes
    // while the work runs, and nothing on the page moves.
    return GlimmerRefreshIndicator(
      onRefresh: () async {
        await Future<void>.delayed(const Duration(milliseconds: 1400));
        if (mounted) setState(() => _refreshed = true);
      },
      child: ListView(
        physics: const AlwaysScrollableScrollPhysics(),
        padding: EdgeInsets.fromLTRB(spacing.large, 24, spacing.large, 24),
        children: [
          Text('An interface\nwith atmosphere.', style: type.titleLarge),
          SizedBox(height: spacing.medium),
          Text(
            _refreshed
                ? 'Refreshed. Surfaces filter what is behind them instead of '
                    'covering it, and pulling this page lit the edge instead of '
                    'sliding a spinner over it.'
                : 'Surfaces filter what is behind them instead of covering it. '
                    'Focus is a lit edge rather than a ripple. Pull this page '
                    'down past the top and the edge lights instead of stretching.',
            style: type.bodySmall.copyWith(color: tokens.colors.outline),
          ),
          SizedBox(height: spacing.extraLarge),

          const _SectionLabel('Card and actions'),
          SizedBox(height: spacing.medium),
          GlimmerCard(
            focused: _saved,
            onTap: () => setState(() => _saved = !_saved),
            title: 'Jabuticaba',
            supportingText: 'Paulista street market, R\$18 a kilo',
            leadingIcon: Icons.local_grocery_store_outlined,
          ),
          SizedBox(height: spacing.medium),
          GlimmerButtonGroup(
            children: [
              GlimmerButton(
                label: _saved ? 'Saved' : 'Save',
                leadingIcon: _saved ? Icons.check : Icons.bookmark_add_outlined,
                prominent: true,
                onPressed: () => setState(() => _saved = !_saved),
              ),
              GlimmerButton(
                label: 'Share',
                leadingIcon: Icons.ios_share,
                onPressed: () {},
              ),
            ],
          ),
          SizedBox(height: spacing.extraLarge),

          const _SectionLabel('Stack'),
          SizedBox(height: spacing.small),
          Text(
            'One item at a time. Swipe up and down.',
            style: type.caption.copyWith(color: tokens.colors.outline),
          ),
          SizedBox(height: spacing.medium),
          GlimmerStack(
            onIndexChanged: (index) => setState(() => _stackIndex = index),
            children: const [
              GlimmerCard(
                title: 'Rafa',
                supportingText: 'Anything from the bakery? I am in the queue.',
                leadingIcon: Icons.chat_bubble_outline,
              ),
              GlimmerCard(
                title: 'Tropicália',
                supportingText: 'Now playing, 3 minutes 23 left',
                leadingIcon: Icons.music_note_outlined,
              ),
              GlimmerCard(
                title: 'Saturday market',
                supportingText: 'Jabuticaba, papaya, kale, pão de queijo',
                leadingIcon: Icons.checklist,
              ),
            ],
          ),
          SizedBox(height: spacing.medium),
          Center(
            child: Text(
              '${_stackIndex + 1} of 3',
              style: type.caption.copyWith(color: tokens.colors.outline),
            ),
          ),

          SizedBox(height: spacing.extraLarge),
          const _SectionLabel('List with an integrated title'),
          SizedBox(height: spacing.medium),
          // The title is the list's own, drawn as a GlimmerTitleChip above the
          // rows. shrinkWrap and NeverScrollable because this one is inside
          // the page's scroll view rather than owning it.
          GlimmerList(
            title: 'Grocery list',
            shrinkWrap: true,
            physics: const NeverScrollableScrollPhysics(),
            padding: EdgeInsets.zero,
            children: [
              for (final item in _groceries)
                GlimmerListItem(
                  label: item,
                  leadingIcon: _bought.contains(item)
                      ? Icons.check_circle
                      : Icons.circle_outlined,
                  selected: _bought.contains(item),
                  onTap: () => setState(
                    () => _bought.contains(item)
                        ? _bought.remove(item)
                        : _bought.add(item),
                  ),
                ),
            ],
          ),
          SizedBox(height: spacing.large),
          const _SectionLabel('Buttons'),
          SizedBox(height: spacing.medium),
          Wrap(
            spacing: spacing.medium,
            runSpacing: spacing.medium,
            children: [
              GlimmerButton(
                label: 'Button',
                leadingIcon: Icons.send_outlined,
                onPressed: () {},
              ),
              GlimmerButton(
                  label: 'Prominent', prominent: true, onPressed: () {}),
              const GlimmerButton(label: 'Disabled', onPressed: null),
              GlimmerToggleButton(
                label: _muted ? 'Muted' : 'Mute',
                leadingIcon: _muted ? Icons.volume_off : Icons.volume_up,
                selected: _muted,
                onChanged: (value) => setState(() => _muted = value),
              ),
            ],
          ),
          SizedBox(height: spacing.medium),
          GlimmerButton(
            label: 'Large, for the one action that matters',
            leadingIcon: Icons.navigation_outlined,
            size: GlimmerButtonSize.large,
            expand: true,
            onPressed: () {},
          ),
          SizedBox(height: spacing.extraLarge),
          const _SectionLabel('Pager'),
          SizedBox(height: spacing.small),
          Text(
            'Pages shorten, blur and fade as they leave the centre.',
            style: tokens.typography.caption
                .copyWith(color: tokens.colors.outline),
          ),
          SizedBox(height: spacing.medium),
          SizedBox(
            height: 220,
            child: GlimmerPager(
              itemCount: _pagerPages.length,
              itemBuilder: (context, page) => GlimmerCard(
                title: _pagerPages[page].$1,
                supportingText: _pagerPages[page].$2,
                leadingIcon: _pagerPages[page].$3,
              ),
            ),
          ),
          SizedBox(height: spacing.extraLarge),
          const _SectionLabel('Icon buttons and voice input'),
          SizedBox(height: spacing.medium),
          Row(
            children: [
              GlimmerIconButton(
                icon: Icons.mic_none,
                tooltip: 'Speak',
                prominent: true,
                onPressed: () => setState(() => _listening = !_listening),
              ),
              SizedBox(width: spacing.medium),
              GlimmerIconToggleButton(
                icon: Icons.bookmark_outline,
                selectedIcon: Icons.bookmark,
                selected: _bought.isNotEmpty,
                tooltip: 'Bookmark',
                onChanged: (_) => setState(_bought.clear),
              ),
              SizedBox(width: spacing.extraLarge),
              GlimmerVoiceInputIndicator(listening: _listening),
            ],
          ),
          SizedBox(height: spacing.extraLarge),
          const _SectionLabel('Choosing'),
          SizedBox(height: spacing.small),
          Text(
            'A tick that is drawn on, a dot that springs out, and a marker that '
            'slides between choices.',
            style: tokens.typography.caption
                .copyWith(color: tokens.colors.outline),
          ),
          SizedBox(height: spacing.medium),
          GlimmerCheckbox(
            value: _digest,
            label: 'Send me the weekly digest',
            onChanged: (value) => setState(() => _digest = value),
          ),
          for (final roast in const ['Light', 'Medium', 'Dark'])
            GlimmerRadio<String>(
              value: roast,
              groupValue: _roast,
              label: '$roast roast',
              onChanged: (value) => setState(() => _roast = value),
            ),
          SizedBox(height: spacing.medium),
          GlimmerTabs(
            labels: const ['All', 'Open', 'Closed'],
            selectedIndex: _filter,
            onChanged: (index) => setState(() => _filter = index),
          ),
          SizedBox(height: spacing.medium),
          GlimmerChipGroup(
            children: [
              for (final tag in const ['Open now', 'Outdoors', 'Free entry'])
                GlimmerChip(
                  label: tag,
                  icon: _tags.contains(tag) ? Icons.check : null,
                  selected: _tags.contains(tag),
                  onPressed: () => setState(
                    () => _tags.contains(tag)
                        ? _tags.remove(tag)
                        : _tags.add(tag),
                  ),
                ),
              GlimmerChip(
                label: 'Santos',
                avatar: const GlimmerAvatar(label: 'Santos', size: 20),
                onDeleted: () {},
              ),
            ],
          ),
          SizedBox(height: spacing.extraLarge),
          const _SectionLabel('Waiting'),
          SizedBox(height: spacing.small),
          Text(
            'A placeholder is the shape of what is coming with light passing '
            'over it, not a grey block with a band sliding behind a window.',
            style: tokens.typography.caption
                .copyWith(color: tokens.colors.outline),
          ),
          SizedBox(height: spacing.medium),
          GlimmerCard(
            child: _loading
                ? Row(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const GlimmerSkeleton(
                        width: 40,
                        height: 40,
                        borderRadius: BorderRadius.all(Radius.circular(999)),
                      ),
                      SizedBox(width: spacing.medium),
                      Expanded(child: GlimmerSkeleton.lines()),
                    ],
                  )
                : Row(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const GlimmerAvatar(label: 'Ana Ribeiro'),
                      SizedBox(width: spacing.medium),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text('Ana Ribeiro',
                                style: tokens.typography.titleSmall),
                            SizedBox(height: spacing.extraSmall),
                            Text(
                              'Left a note about the delivery window on '
                              'Thursday morning.',
                              style: tokens.typography.bodySmall,
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
          ),
          SizedBox(height: spacing.medium),
          GlimmerButton(
            label: _loading ? 'Load the content' : 'Back to waiting',
            expand: true,
            onPressed: () => setState(() => _loading = !_loading),
          ),
          SizedBox(height: spacing.extraLarge),
          const _SectionLabel('Small pieces'),
          SizedBox(height: spacing.medium),
          GlimmerSearchField(
            hint: 'Search the list',
            onChanged: (value) => setState(() => _query = value),
          ),
          SizedBox(height: spacing.medium),
          Row(
            children: [
              const GlimmerAvatar(label: 'Ana Ribeiro'),
              SizedBox(width: spacing.medium),
              const GlimmerAvatar(icon: Icons.storefront, size: 32),
              SizedBox(width: spacing.large),
              GlimmerBadge(
                count: _query.isEmpty ? 3 : _query.length,
                child: GlimmerIconButton(
                  icon: Icons.notifications_outlined,
                  tooltip: 'Notifications',
                  onPressed: () {},
                ),
              ),
              SizedBox(width: spacing.large),
              const GlimmerCircularProgress(size: 32),
              SizedBox(width: spacing.medium),
              const GlimmerCircularProgress(value: 0.65, size: 32),
            ],
          ),
          SizedBox(height: spacing.large),
          const GlimmerDivider(),
          SizedBox(height: spacing.large),
          const GlimmerExpansionTile(
            label: 'Delivery',
            supportingLabel: 'Thursday, before noon',
            leadingIcon: Icons.local_shipping_outlined,
            children: [
              GlimmerListItem(
                label: 'Leave with a neighbour',
                leadingIcon: Icons.home_outlined,
              ),
              GlimmerListItem(
                label: 'Ring the bell twice',
                leadingIcon: Icons.notifications_active_outlined,
              ),
            ],
          ),
          SizedBox(height: spacing.extraLarge),
          const _SectionLabel('Screens'),
          SizedBox(height: spacing.small),
          Text(
            'A pushed screen arrives instead of sliding, and the one behind it '
            'withdraws rather than moving away.',
            style: tokens.typography.caption
                .copyWith(color: tokens.colors.outline),
          ),
          SizedBox(height: spacing.medium),
          GlimmerButton(
            label: 'Open a screen',
            leadingIcon: Icons.open_in_new,
            onPressed: () => Navigator.of(context).push(
              GlimmerPageRoute<void>(
                  builder: (context) => const _DetailScreen()),
            ),
          ),
          SizedBox(height: spacing.extraLarge),
          const _SectionLabel('Overlays'),
          SizedBox(height: spacing.small),
          Text(
            'A panel over a blurred app, a sheet from the bottom, a message that '
            'leaves on its own, a menu against its anchor.',
            style: tokens.typography.caption
                .copyWith(color: tokens.colors.outline),
          ),
          SizedBox(height: spacing.medium),
          Wrap(
            spacing: spacing.medium,
            runSpacing: spacing.medium,
            children: [
              GlimmerButton(
                label: 'Dialog',
                leadingIcon: Icons.chat_outlined,
                onPressed: () => _openDialog(context),
              ),
              GlimmerButton(
                label: 'Sheet',
                leadingIcon: Icons.vertical_align_bottom,
                onPressed: () => _openSheet(context),
              ),
              GlimmerButton(
                label: 'Message',
                leadingIcon: Icons.campaign_outlined,
                onPressed: () => showGlimmerSnackbar(
                  context,
                  message: 'Added to the Saturday market list',
                  icon: Icons.check_circle_outline,
                  actionLabel: 'Undo',
                  onAction: () {},
                  bottomInset: _navigationStripHeight,
                ),
              ),
              Builder(
                builder: (context) => GlimmerButton(
                  label: 'Menu',
                  leadingIcon: Icons.more_horiz,
                  onPressed: () => _openMenu(context),
                ),
              ),
            ],
          ),
          SizedBox(height: spacing.extraLarge),
          const _SectionLabel('Input'),
          SizedBox(height: spacing.small),
          Text(
            'A field, a switch, a bar and a slider, all drawn from the same '
            'tokens as everything above.',
            style: tokens.typography.caption
                .copyWith(color: tokens.colors.outline),
          ),
          SizedBox(height: spacing.medium),
          const GlimmerTextField(
            label: 'Add an item',
            prefixIcon: Icons.add,
          ),
          SizedBox(height: spacing.medium),
          // The switch drives the card directly below it, so it is obvious
          // what it is for. A toggle in a gallery that changes nothing is
          // furniture.
          GlimmerListItem(
            label: 'Ambient sweep',
            supportingLabel:
                'A highlight travels the edge of a focused surface',
            trailing: GlimmerSwitch(
              value: _ambient,
              label: 'Ambient sweep',
              onChanged: (value) => setState(() => _ambient = value),
            ),
          ),
          SizedBox(height: spacing.medium),
          GlimmerCard(
            focused: true,
            enableAmbientPulse: _ambient,
            leadingIcon: Icons.auto_awesome,
            title: 'Watch the edge',
            supportingText: _ambient
                ? 'The highlight runs once, waits, and runs again.'
                : 'Turn the sweep on to see it.',
          ),
          SizedBox(height: spacing.large),
          GlimmerProgressBar(value: _progress),
          SizedBox(height: spacing.medium),
          GlimmerSlider(
            value: _progress,
            semanticLabel: 'Progress',
            onChanged: (value) => setState(() => _progress = value),
          ),
          SizedBox(height: spacing.large),
          Text(
            'With no value, a highlight travels the track instead of a block '
            'sliding along it.',
            style: tokens.typography.caption
                .copyWith(color: tokens.colors.outline),
          ),
          SizedBox(height: spacing.medium),
          const GlimmerProgressBar(),
          // Room for the floating button, which would otherwise sit on the
          // last thing in the list.
          SizedBox(height: GlimmerFab.size + spacing.large),
        ],
      ),
    );
  }
}

class _FoundationsPage extends StatelessWidget {
  const _FoundationsPage();

  @override
  Widget build(BuildContext context) {
    final tokens = GlimmerTheme.of(context);
    final colors = tokens.colors;
    final type = tokens.typography;
    final spacing = tokens.spacing;

    final swatches = <String, Color>{
      'primary': colors.primary,
      'secondary': colors.secondary,
      'positive': colors.positive,
      'negative': colors.negative,
      'surface': colors.surface,
      'outline': colors.outline,
    };

    return ListView(
      padding: EdgeInsets.fromLTRB(spacing.large, 24, spacing.large, 24),
      children: [
        const _SectionLabel('Colour'),
        SizedBox(height: spacing.medium),
        Wrap(
          spacing: spacing.medium,
          runSpacing: spacing.medium,
          children: [
            for (final entry in swatches.entries)
              Column(
                children: [
                  Container(
                    width: 64,
                    height: 44,
                    decoration: BoxDecoration(
                      color: entry.value,
                      borderRadius: tokens.shapes.small,
                      border: Border.all(color: colors.outline, width: 1.5),
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(entry.key, style: type.caption),
                ],
              ),
          ],
        ),
        SizedBox(height: spacing.extraLarge),
        const _SectionLabel('Edge'),
        SizedBox(height: spacing.small),
        Text(
          'The border is not one colour. It is a gradient with a different '
          'colour at each corner, brightest where the light lands, and it '
          'turns a quarter as a surface takes focus.',
          style: type.caption.copyWith(color: colors.outline),
        ),
        SizedBox(height: spacing.medium),
        Row(
          children: [
            const Expanded(
              child: GlimmerCard(
                title: 'Resting',
                supportingText: '1.5 px',
              ),
            ),
            SizedBox(width: spacing.medium),
            const Expanded(
              child: GlimmerCard(
                focused: true,
                title: 'Focused',
                supportingText: '2 px, focal',
              ),
            ),
          ],
        ),
      ],
    );
  }
}

/// The screen [GlimmerPageRoute] pushes, so the arrival has something to
/// arrive with.
class _DetailScreen extends StatelessWidget {
  const _DetailScreen();

  @override
  Widget build(BuildContext context) {
    final tokens = GlimmerTheme.of(context);
    final spacing = tokens.spacing;

    return GlimmerScaffold(
      backdrop: const GlimmerBackdrop(),
      title: 'Museu do Café',
      leading: GlimmerIconButton(
        icon: Icons.arrow_back,
        tooltip: 'Back',
        onPressed: () => Navigator.of(context).pop(),
      ),
      body: ListView(
        padding: EdgeInsets.fromLTRB(spacing.large, 24, spacing.large, 24),
        children: [
          const GlimmerCard(
            leadingIcon: Icons.museum_outlined,
            title: 'Santos, open until five',
            supportingText: 'The old coffee exchange, kept as it was.',
          ),
          SizedBox(height: spacing.medium),
          const GlimmerCard(
            leadingIcon: Icons.directions_walk,
            title: 'Arrive 10:08',
            supportingText: 'Six minutes on foot, mostly shade',
          ),
          SizedBox(height: spacing.medium),
          GlimmerButton(
            label: 'Back',
            expand: true,
            onPressed: () => Navigator.of(context).pop(),
          ),
        ],
      ),
    );
  }
}

class _SectionLabel extends StatelessWidget {
  const _SectionLabel(this.text);

  final String text;

  @override
  Widget build(BuildContext context) {
    final tokens = GlimmerTheme.of(context);
    return Text(
      text.toUpperCase(),
      style: tokens.typography.caption.copyWith(
        color: tokens.colors.secondary,
        letterSpacing: 1.2,
      ),
    );
  }
}

class _AboutSheet extends StatelessWidget {
  const _AboutSheet();

  @override
  Widget build(BuildContext context) {
    final tokens = GlimmerTheme.of(context);
    return Padding(
      padding: EdgeInsets.all(tokens.spacing.large),
      child: GlimmerSurface(
        additive: false,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('material_glimmer_ui', style: tokens.typography.titleMedium),
            SizedBox(height: tokens.spacing.extraSmall),
            Text(
              'Version 0.6.0 · MIT',
              style: tokens.typography.caption.copyWith(
                color: tokens.colors.secondary,
              ),
            ),
            SizedBox(height: tokens.spacing.medium),
            Text(
              'A design system for Flutter: a theme, a full token set and a '
              'widget library, picked the way you pick Material or Cupertino. '
              'Surfaces are glass over whatever you put behind them, focus is '
              'a lit edge, and depth is the plane behind withdrawing rather '
              'than a shadow in front.',
              style: tokens.typography.bodySmall,
            ),
            SizedBox(height: tokens.spacing.medium),
            Text(
              'Everything is drawn rather than shipped as assets, and Flutter '
              'is the only dependency.',
              style: tokens.typography.bodySmall.copyWith(
                color: tokens.colors.outline,
              ),
            ),
            SizedBox(height: tokens.spacing.large),
            GlimmerButton(
              label: 'Close',
              expand: true,
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        ),
      ),
    );
  }
}
1
likes
160
points
125
downloads

Documentation

API reference

Publisher

verified publisheradrianogba.is-a.dev

Weekly Downloads

A Flutter UI kit of glass surfaces and lit edges. An app widget, a theme, a full token set and a widget library. Inspired by Jetpack Compose Glimmer and Material Design.

Repository (GitHub)
View/report issues

Topics

#glimmer #design-system #ui #theme #widget

Funding

Consider supporting this project:

github.com

License

MIT (license)

Dependencies

flutter

More

Packages that depend on material_glimmer_ui