sole_sheet 0.1.2 copy "sole_sheet: ^0.1.2" to clipboard
sole_sheet: ^0.1.2 copied to clipboard

A premium glass bottom sheet for Flutter — floating 3D badge, staggered reveals, confetti, light/dark/glassy surfaces and RTL-aware text. Zero dependencies.

example/lib/main.dart

import 'dart:async';

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

import 'backdrop.dart';
import 'showcase.dart';

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

class SoleSheetDemoApp extends StatefulWidget {
  const SoleSheetDemoApp({super.key});

  @override
  State<SoleSheetDemoApp> createState() => _SoleSheetDemoAppState();
}

class _SoleSheetDemoAppState extends State<SoleSheetDemoApp> {
  ThemeMode _themeMode = ThemeMode.dark;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sole Sheet',
      debugShowCheckedModeBanner: false,
      themeMode: _themeMode,
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF3291FF),
        brightness: Brightness.light,
      ),
      darkTheme: ThemeData(
        colorSchemeSeed: const Color(0xFF3291FF),
        brightness: Brightness.dark,
      ),
      builder: SoleSheet.init(),
      home: DemoPage(
        themeMode: _themeMode,
        onToggleTheme: () => setState(() => _themeMode =
            _themeMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark),
        onSetTheme: (mode) => setState(() => _themeMode = mode),
      ),
    );
  }
}

class DemoPage extends StatefulWidget {
  const DemoPage({
    super.key,
    required this.themeMode,
    required this.onToggleTheme,
    required this.onSetTheme,
  });

  final ThemeMode themeMode;
  final VoidCallback onToggleTheme;
  final ValueChanged<ThemeMode> onSetTheme;

  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  SoleSheetMode _mode = SoleSheetMode.glassy;
  double _speed = 1.0;
  bool _haptics = true;
  bool _sparkle = true;
  bool _float = true;
  bool _confetti = false;
  bool _dismissible = true;
  bool _showDefaultAction = true;
  bool _customButtons = false;
  bool _chips = false;
  bool _customBadge = false;
  bool _arabic = false;

  @override
  void initState() {
    super.initState();
    _applyConfig();
    if (kShowcase.isNotEmpty) {
      Future.delayed(const Duration(milliseconds: 700), () {
        if (mounted) _runShowcase(kShowcase);
      });
    }
  }

  void _applyConfig() {
    SoleSheet.config = SoleSheet.config.copyWith(
      mode: _mode,
      durationScale: _speed,
      enableHaptics: _haptics,
      enableSparkle: _sparkle,
      enableFloat: _float,
    );
  }

  // ── Showcase (README screenshots) ─────────────────────────────────────────

  /// Presents one deterministic `<mode>_<type>` combination (or just the
  /// controls page for `demo`) — driven by [kShowcase] + hot restart.
  void _runShowcase(String variant) {
    if (variant == 'demo') return;
    final split = variant.indexOf('_');
    final modeName = variant.substring(0, split);
    final type = variant.substring(split + 1);

    widget
        .onSetTheme(modeName == 'light' ? ThemeMode.light : ThemeMode.dark);
    setState(() {
      _mode = switch (modeName) {
        'light' => SoleSheetMode.light,
        'dark' => SoleSheetMode.dark,
        _ => SoleSheetMode.glassy,
      };
    });
    _applyConfig();
    if (modeName == 'glassy') {
      // A lighter barrier for the glassy captures so the frosted blur over
      // the colorful backdrop is unmistakable (barrierColor is a public
      // config/per-call knob).
      SoleSheet.config = SoleSheet.config
          .copyWith(barrierColor: const Color(0x14000000));
    }
    // Give the theme swap a frame before presenting.
    Future.delayed(const Duration(milliseconds: 300), () {
      if (!mounted) return;
      _showType(type);
    });
  }

  // ── Sheet triggers ────────────────────────────────────────────────────────

  List<Widget>? get _extraButtons => _customButtons
      ? [
          SoleSheetButton(
            label: _arabic ? 'عرض التفاصيل' : 'View details',
            onTap: () => SoleSheet.dismiss(),
          ),
          SoleSheetButton.secondary(
            label: _arabic ? 'لاحقاً' : 'Maybe later',
            onTap: () => SoleSheet.dismiss(),
          ),
        ]
      : null;

  List<String>? get _chipRow =>
      _chips ? const ['PRIORITY', 'ACTION NEEDED'] : null;

  Widget? get _badgeWidget => _customBadge
      ? const Text('🦄', style: TextStyle(fontSize: 44))
      : null;

  bool? get _confettiOverride => _confetti ? true : null;

  void _snack(String message) {
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text(message)));
  }

  void _showType(String type) {
    switch (type) {
      case 'success':
        SoleSheet.success(
          _arabic
              ? 'تمت الموافقة على طلب **الإجازة** الخاص بك بنجاح.'
              : 'Your **leave request** has been submitted for approval.',
          title: _arabic ? 'تمت العملية بنجاح' : null,
          actionText: _arabic ? 'تم' : null,
          confetti: _confettiOverride,
          buttons: _extraButtons,
          showDefaultAction: _showDefaultAction,
          chips: _chipRow,
          iconWidget: _badgeWidget,
          isDismissible: _dismissible,
        );
      case 'error':
        SoleSheet.error(
          _arabic
              ? 'تعذر الوصول إلى الخادم.\nتحقق من اتصالك وحاول مرة أخرى.'
              : 'The server could not be reached.\nCheck your connection and try again.',
          title: _arabic ? 'حدث خطأ ما' : null,
          actionText: _arabic ? 'حاول مجدداً' : null,
          onAction: () => _snack('Retrying…'),
          confetti: _confettiOverride,
          buttons: _extraButtons,
          showDefaultAction: _showDefaultAction,
          chips: _chipRow,
          iconWidget: _badgeWidget,
          isDismissible: _dismissible,
        );
      case 'warning':
        SoleSheet.warning(
          _arabic
              ? 'ستنتهي جلستك خلال **٥ دقائق**. احفظ عملك الآن.'
              : 'Your session expires in **5 minutes**. Save your work now.',
          title: _arabic ? 'تنبيه هام' : null,
          actionText: _arabic ? 'حسناً' : null,
          confetti: _confettiOverride,
          buttons: _extraButtons,
          showDefaultAction: _showDefaultAction,
          chips: _chipRow,
          iconWidget: _badgeWidget,
          isDismissible: _dismissible,
        );
      case 'info':
        SoleSheet.info(
          _arabic
              ? 'تدخل سياسة الخصوصية الجديدة حيز التنفيذ في **الأول من أغسطس**.'
              : 'A new privacy policy takes effect on **August 1st**.',
          title: _arabic ? 'معلومة' : null,
          actionText: _arabic ? 'إغلاق' : null,
          confetti: _confettiOverride,
          buttons: _extraButtons,
          showDefaultAction: _showDefaultAction,
          chips: _chipRow,
          iconWidget: _badgeWidget,
          isDismissible: _dismissible,
        );
      case 'confirm':
        _confirmFlow();
      case 'celebration':
        SoleSheet.celebrate(
          _arabic
              ? 'تهانينا على إتمام **٥ سنوات** مع الفريق!'
              : 'Congratulations on **5 years** with the team!',
          title: _arabic ? '🎉 ذكرى العمل السنوية' : 'Work Anniversary 🎉',
          chips: _chipRow ?? const ['MILESTONE', 'TEAM EVENT'],
          buttons: _extraButtons,
          showDefaultAction: _showDefaultAction,
          iconWidget: _badgeWidget,
          isDismissible: _dismissible,
        );
      case 'custom':
        SoleSheet.custom(
          _arabic
              ? 'أي عنصر واجهة يعمل داخل الشارة — صور رمزية أو رموز تعبيرية.'
              : 'Any widget works in the badge — avatars, Lottie, emoji.',
          title: _arabic ? 'انطلق' : 'Ship It',
          accent: const Color(0xFFA78BFA),
          iconWidget:
              _badgeWidget ?? const Text('🚀', style: TextStyle(fontSize: 44)),
          actionText: _arabic ? 'انطلاق' : 'Blast off',
          confetti: _confettiOverride,
          buttons: _extraButtons,
          showDefaultAction: _showDefaultAction,
          chips: _chipRow,
          isDismissible: _dismissible,
        );
    }
  }

  Future<void> _confirmFlow() async {
    final confirmed = await SoleSheet.confirm(
      _arabic
          ? 'سيتم حذف المسودة **نهائياً** ولا يمكن التراجع.'
          : 'This will **permanently** delete the draft.',
      title: _arabic ? 'هل أنت متأكد؟' : 'Delete Draft?',
      confirmText: _arabic ? 'نعم، احذف' : 'YES, DELETE',
      cancelText: _arabic ? 'إغلاق' : 'Close',
      isDismissible: _dismissible,
    );
    if (!mounted) return;
    _snack(confirmed ? 'Confirmed — draft deleted' : 'Cancelled — kept safe');
  }

  // ── UI ────────────────────────────────────────────────────────────────────

  @override
  Widget build(BuildContext context) {
    final dark = Theme.of(context).brightness == Brightness.dark;
    final panelColor = dark ? const Color(0xF20E1017) : const Color(0xF5FFFFFF);
    return Scaffold(
      extendBodyBehindAppBar: true,
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        title: const Text('Sole Sheet'),
        actions: [
          IconButton(
            tooltip: 'Toggle app theme (glassy auto-resolves)',
            onPressed: widget.onToggleTheme,
            icon: const Icon(Icons.brightness_6_outlined),
          ),
        ],
      ),
      body: Stack(
        fit: StackFit.expand,
        children: [
          DemoBackdrop(dark: dark),
          // Screenshot variants hide the controls so the vivid backdrop sits
          // directly behind the sheet (that's what the glassy mode frosts).
          if (kShowcase.isEmpty || kShowcase == 'demo')
          SafeArea(
            child: ListView(
              padding: const EdgeInsets.fromLTRB(16, 60, 16, 32),
              children: [
                _Panel(
                  color: panelColor,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('Surface mode',
                          style: Theme.of(context).textTheme.titleMedium),
                      const SizedBox(height: 8),
                      SegmentedButton<SoleSheetMode>(
                        segments: const [
                          ButtonSegment(
                              value: SoleSheetMode.light, label: Text('Light')),
                          ButtonSegment(
                              value: SoleSheetMode.dark, label: Text('Dark')),
                          ButtonSegment(
                              value: SoleSheetMode.glassy,
                              label: Text('Glassy')),
                        ],
                        selected: {_mode},
                        onSelectionChanged: (s) {
                          setState(() => _mode = s.first);
                          _applyConfig();
                        },
                      ),
                      ListTile(
                        contentPadding: EdgeInsets.zero,
                        title: Text('Speed  ${_speed.toStringAsFixed(2)}×'),
                        subtitle: Slider(
                          value: _speed,
                          min: 0.1,
                          max: 2.0,
                          onChanged: (v) => setState(() => _speed = v),
                          onChangeEnd: (_) => _applyConfig(),
                        ),
                      ),
                    ],
                  ),
                ),
                const SizedBox(height: 12),
                _Panel(
                  color: panelColor,
                  child: Column(
                    children: [
                      _switch('Haptics', _haptics, (v) {
                        setState(() => _haptics = v);
                        _applyConfig();
                      }),
                      _switch('Sparkle on badge tap', _sparkle, (v) {
                        setState(() => _sparkle = v);
                        _applyConfig();
                      }),
                      _switch('Badge float', _float, (v) {
                        setState(() => _float = v);
                        _applyConfig();
                      }),
                      _switch('Confetti (any type)', _confetti,
                          (v) => setState(() => _confetti = v)),
                      _switch('Dismissible (barrier / drag / back)',
                          _dismissible, (v) => setState(() => _dismissible = v)),
                      _switch('Show default action', _showDefaultAction,
                          (v) => setState(() => _showDefaultAction = v)),
                      _switch('Custom buttons (primary + ghost)',
                          _customButtons, (v) => setState(() => _customButtons = v)),
                      _switch(
                          'Chips row', _chips, (v) => setState(() => _chips = v)),
                      _switch('Custom badge widget (🦄)', _customBadge,
                          (v) => setState(() => _customBadge = v)),
                      _switch('RTL Arabic content', _arabic,
                          (v) => setState(() => _arabic = v)),
                    ],
                  ),
                ),
                const SizedBox(height: 12),
                _Panel(
                  color: panelColor,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('Types',
                          style: Theme.of(context).textTheme.titleMedium),
                      const SizedBox(height: 10),
                      Wrap(
                        spacing: 10,
                        runSpacing: 10,
                        children: [
                          for (final t in const [
                            ('success', 'Success'),
                            ('error', 'Error'),
                            ('warning', 'Warning'),
                            ('info', 'Info'),
                            ('confirm', 'Confirm'),
                            ('celebration', 'Celebration'),
                            ('custom', 'Custom'),
                          ])
                            FilledButton.tonal(
                              onPressed: () => _showType(t.$1),
                              child: Text(t.$2),
                            ),
                        ],
                      ),
                      const SizedBox(height: 16),
                      Text('Extras',
                          style: Theme.of(context).textTheme.titleMedium),
                      const SizedBox(height: 10),
                      Wrap(
                        spacing: 10,
                        runSpacing: 10,
                        children: [
                          OutlinedButton(
                            onPressed: () {
                              SoleSheet.info(
                                'This sheet closes itself in two seconds via '
                                'SoleSheet.dismiss().',
                                title: 'Programmatic Dismiss',
                              );
                              Timer(const Duration(seconds: 2),
                                  SoleSheet.dismiss);
                            },
                            child: const Text('Dismiss in 2s'),
                          ),
                          OutlinedButton(
                            onPressed: () => SoleSheet.info(
                              'Close this sheet any way you like — a snackbar '
                              'confirms the onDismiss callback fired.',
                              title: 'onDismiss Demo',
                              onDismiss: () => _snack('onDismiss fired 👋'),
                            ),
                            child: const Text('onDismiss demo'),
                          ),
                          OutlinedButton(
                            onPressed: () => SoleSheet.warning(
                              '1. **Name** is required\n'
                              '2. **Start date** must be in the future\n'
                              '3. Attachment exceeds the 5 MB limit\n'
                              '4. Cost centre code is unknown',
                              title: 'Please Fix These',
                            ),
                            child: const Text('List message'),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _switch(String label, bool value, ValueChanged<bool> onChanged) {
    return SwitchListTile(
      contentPadding: EdgeInsets.zero,
      dense: true,
      title: Text(label),
      value: value,
      onChanged: onChanged,
    );
  }
}

class _Panel extends StatelessWidget {
  const _Panel({required this.color, required this.child});

  final Color color;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: color,
        borderRadius: BorderRadius.circular(20),
        border: Border.all(color: const Color(0x26FFFFFF)),
      ),
      child: child,
    );
  }
}
1
likes
160
points
176
downloads

Documentation

API reference

Publisher

verified publishersohailahmad.com

Weekly Downloads

A premium glass bottom sheet for Flutter — floating 3D badge, staggered reveals, confetti, light/dark/glassy surfaces and RTL-aware text. Zero dependencies.

Repository (GitHub)
View/report issues

Topics

#bottom-sheet #notification #dialog #glassmorphism #ui

License

MIT (license)

Dependencies

flutter

More

Packages that depend on sole_sheet