244 lines
6.2 KiB
Dart
244 lines
6.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
Future<T?> showSidePanel<T>(
|
|
BuildContext context,
|
|
Widget panel, {
|
|
double? width,
|
|
}) {
|
|
final screenWidth = MediaQuery.sizeOf(context).width;
|
|
final defaultWidth = screenWidth > 1200
|
|
? 480.0
|
|
: (screenWidth * 0.38).clamp(360.0, 480.0);
|
|
final panelWidth = (width ?? defaultWidth).clamp(360.0, screenWidth * 0.95);
|
|
|
|
return showGeneralDialog<T>(
|
|
context: context,
|
|
useRootNavigator: true,
|
|
barrierDismissible: true,
|
|
barrierLabel: 'Dismiss',
|
|
barrierColor: Colors.black.withValues(alpha: 0.35),
|
|
transitionDuration: const Duration(milliseconds: 280),
|
|
pageBuilder: (context, _, __) {
|
|
final theme = Theme.of(context);
|
|
return Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Material(
|
|
elevation: 16,
|
|
color: theme.colorScheme.surface,
|
|
borderRadius: const BorderRadius.horizontal(left: Radius.circular(16)),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: SizedBox(
|
|
width: panelWidth,
|
|
height: MediaQuery.sizeOf(context).height,
|
|
child: panel,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
transitionBuilder: (context, anim, _, child) {
|
|
return SlideTransition(
|
|
position: Tween<Offset>(
|
|
begin: const Offset(1, 0),
|
|
end: Offset.zero,
|
|
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOutCubic)),
|
|
child: child,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
class SidePanelScaffold extends StatelessWidget {
|
|
const SidePanelScaffold({
|
|
super.key,
|
|
required this.title,
|
|
required this.child,
|
|
this.footer,
|
|
this.onClose,
|
|
});
|
|
|
|
final String title;
|
|
final Widget child;
|
|
final Widget? footer;
|
|
final VoidCallback? onClose;
|
|
|
|
void _close(BuildContext context) {
|
|
if (onClose != null) {
|
|
onClose!();
|
|
return;
|
|
}
|
|
Navigator.of(context, rootNavigator: true).pop();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(24, 20, 12, 16),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => _close(context),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Divider(height: 1),
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: child,
|
|
),
|
|
),
|
|
if (footer != null) ...[
|
|
const Divider(height: 1),
|
|
Padding(padding: const EdgeInsets.all(24), child: footer),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class SidePanelSection extends StatelessWidget {
|
|
const SidePanelSection({
|
|
super.key,
|
|
required this.title,
|
|
required this.children,
|
|
});
|
|
|
|
final String title;
|
|
final List<Widget> children;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final dividerColor =
|
|
Theme.of(context).colorScheme.outline.withValues(alpha: 0.2);
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.8,
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Divider(height: 1, color: dividerColor),
|
|
const SizedBox(height: 16),
|
|
...children,
|
|
const SizedBox(height: 24),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class SidePanelFormRow extends StatelessWidget {
|
|
const SidePanelFormRow({
|
|
super.key,
|
|
required this.left,
|
|
required this.right,
|
|
this.spacing = 16,
|
|
});
|
|
|
|
final Widget left;
|
|
final Widget right;
|
|
final double spacing;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final stack = constraints.maxWidth < 420;
|
|
|
|
if (stack) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
left,
|
|
const SizedBox(height: 12),
|
|
right,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(child: left),
|
|
SizedBox(width: spacing),
|
|
Expanded(child: right),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Responsive row with up to three equal-width form fields.
|
|
class FormRowThree extends StatelessWidget {
|
|
const FormRowThree({
|
|
super.key,
|
|
required this.children,
|
|
this.spacing = 12,
|
|
this.stackBelowWidth = 768,
|
|
});
|
|
|
|
final List<Widget> children;
|
|
final double spacing;
|
|
final double stackBelowWidth;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
if (constraints.maxWidth < stackBelowWidth) {
|
|
return Padding(
|
|
padding: EdgeInsets.only(bottom: spacing),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
for (var i = 0; i < children.length; i++) ...[
|
|
if (i > 0) SizedBox(height: spacing),
|
|
children[i],
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.only(bottom: spacing),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
for (var i = 0; i < children.length; i++) ...[
|
|
if (i > 0) SizedBox(width: spacing),
|
|
Expanded(child: children[i]),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|