Files
superport_v2/lib/widgets/components/superport_dialog.dart

329 lines
8.9 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
import 'package:shadcn_ui/shadcn_ui.dart';
import 'keyboard_shortcuts.dart';
const double _kDialogMaxWidth = 640;
const double _kDialogMobileBreakpoint = 640;
const EdgeInsets _kDialogDesktopInset = EdgeInsets.symmetric(
horizontal: 24,
vertical: 32,
);
const EdgeInsets _kDialogBodyPadding = EdgeInsets.symmetric(
horizontal: 20,
vertical: 24,
);
/// 공통 모달 다이얼로그 scaffold.
///
/// - ShadCard 기반으로 헤더/본문/푸터 영역을 분리한다.
/// - 모바일에서는 전체 화면으로 확장되며 시스템 인셋을 자동 반영한다.
/// - 닫기 버튼 및 사용자 정의 액션을 지원한다.
class SuperportDialog extends StatelessWidget {
const SuperportDialog({
super.key,
required this.title,
this.description,
this.child = const SizedBox.shrink(),
this.primaryAction,
this.secondaryAction,
this.mobileFullscreen = true,
this.constraints,
this.actions,
this.contentPadding,
this.header,
this.footer,
this.showCloseButton = true,
this.onClose,
this.scrollable = true,
this.insetPadding,
this.onSubmit,
this.enableFocusTrap = true,
});
final String title;
final String? description;
final Widget child;
final Widget? primaryAction;
final Widget? secondaryAction;
final bool mobileFullscreen;
final BoxConstraints? constraints;
final List<Widget>? actions;
final EdgeInsetsGeometry? contentPadding;
final Widget? header;
final Widget? footer;
final bool showCloseButton;
final VoidCallback? onClose;
final bool scrollable;
final EdgeInsets? insetPadding;
final FutureOr<void> Function()? onSubmit;
final bool enableFocusTrap;
/// 공통 다이얼로그를 노출하는 헬퍼. `showDialog`와 동일하게 동작한다.
static Future<T?> show<T>({
required BuildContext context,
required SuperportDialog dialog,
bool barrierDismissible = true,
}) {
return showDialog<T>(
context: context,
barrierDismissible: barrierDismissible,
builder: (_) => dialog,
);
}
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final theme = ShadTheme.of(context);
final screenWidth = mediaQuery.size.width;
final isMobile = screenWidth <= _kDialogMobileBreakpoint;
void handleClose() {
if (onClose != null) {
onClose!();
return;
}
Navigator.of(context).maybePop();
}
final resolvedHeader =
header ??
_SuperportDialogHeader(
title: title,
description: description,
showCloseButton: showCloseButton,
onClose: handleClose,
);
final resolvedFooter = footer ?? _buildFooter(context);
final card = ShadCard(
padding: EdgeInsets.zero,
child: ClipRRect(
borderRadius: theme.radius,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
resolvedHeader,
Flexible(
child: _DialogBody(
padding: contentPadding ?? _kDialogBodyPadding,
scrollable: scrollable,
child: child,
),
),
if (resolvedFooter != null) resolvedFooter,
],
),
),
);
final resolvedConstraints =
constraints ??
BoxConstraints(
maxWidth: isMobile && mobileFullscreen
? screenWidth
: _kDialogMaxWidth,
minWidth: isMobile && mobileFullscreen ? screenWidth : 360,
);
final resolvedInset =
insetPadding ??
(isMobile && mobileFullscreen ? EdgeInsets.zero : _kDialogDesktopInset);
return Dialog(
insetPadding: resolvedInset,
backgroundColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
child: SafeArea(
child: AnimatedPadding(
duration: const Duration(milliseconds: 120),
curve: Curves.easeOut,
padding: mediaQuery.viewInsets,
child: ConstrainedBox(
constraints: resolvedConstraints,
child: DialogKeyboardShortcuts(
onEscape: handleClose,
onSubmit: onSubmit,
enableFocusTrap: enableFocusTrap,
child: card,
),
),
),
),
);
}
Widget? _buildFooter(BuildContext context) {
if (actions != null) {
final filtered = actions!.whereType<Widget>().toList();
if (filtered.isEmpty) {
return null;
}
return _DialogFooter(children: filtered);
}
final fallback = <Widget>[
if (secondaryAction != null) secondaryAction!,
primaryAction ??
ShadButton.ghost(
onPressed: onClose ?? () => Navigator.of(context).maybePop(),
child: const Text('닫기'),
),
].whereType<Widget>().toList();
if (fallback.isEmpty) {
return null;
}
return _DialogFooter(children: fallback);
}
}
class _DialogBody extends StatelessWidget {
const _DialogBody({
required this.child,
required this.padding,
required this.scrollable,
});
final Widget child;
final EdgeInsetsGeometry padding;
final bool scrollable;
@override
Widget build(BuildContext context) {
final content = Padding(padding: padding, child: child);
if (!scrollable) {
return content;
}
return SingleChildScrollView(child: content);
}
}
class _DialogFooter extends StatelessWidget {
const _DialogFooter({required this.children});
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
decoration: BoxDecoration(
color: ShadTheme.of(context).colorScheme.muted,
border: Border(
top: BorderSide(color: ShadTheme.of(context).colorScheme.border),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
for (var i = 0; i < children.length; i++)
Padding(
padding: EdgeInsets.only(left: i == 0 ? 0 : 12),
child: children[i],
),
],
),
);
}
}
class _SuperportDialogHeader extends StatelessWidget {
const _SuperportDialogHeader({
required this.title,
this.description,
required this.showCloseButton,
required this.onClose,
});
final String title;
final String? description;
final bool showCloseButton;
final VoidCallback onClose;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
decoration: BoxDecoration(
color: theme.colorScheme.card,
border: Border(bottom: BorderSide(color: theme.colorScheme.border)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: theme.textTheme.h3),
if (description != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(description!, style: theme.textTheme.muted),
),
],
),
),
if (showCloseButton)
IconButton(
icon: const Icon(lucide.LucideIcons.x, size: 18),
tooltip: '닫기',
onPressed: onClose,
),
],
),
);
}
}
/// 페이지에서 반복되는 호출 패턴을 줄이기 위한 편의 함수.
Future<T?> showSuperportDialog<T>({
required BuildContext context,
required String title,
String? description,
required Widget body,
Widget? primaryAction,
Widget? secondaryAction,
List<Widget>? actions,
bool mobileFullscreen = true,
bool barrierDismissible = true,
BoxConstraints? constraints,
EdgeInsetsGeometry? contentPadding,
bool scrollable = true,
bool showCloseButton = true,
VoidCallback? onClose,
FutureOr<void> Function()? onSubmit,
bool enableFocusTrap = true,
}) {
return SuperportDialog.show<T>(
context: context,
barrierDismissible: barrierDismissible,
dialog: SuperportDialog(
title: title,
description: description,
primaryAction: primaryAction,
secondaryAction: secondaryAction,
actions: actions,
constraints: constraints,
mobileFullscreen: mobileFullscreen,
contentPadding: contentPadding,
scrollable: scrollable,
showCloseButton: showCloseButton,
onClose: onClose,
onSubmit: onSubmit,
enableFocusTrap: enableFocusTrap,
child: body,
),
);
}