sole_sheet 0.1.2
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.
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,
);
}
}