Files
superport_v2/lib/widgets/app_layout.dart
2025-09-29 01:51:47 +09:00

128 lines
3.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'components/page_header.dart';
/// 앱 공통 레이아웃: 브레드크럼/헤더/툴바/본문을 일관되게 배치한다.
class AppLayout extends StatelessWidget {
const AppLayout({
super.key,
required this.title,
this.subtitle,
this.breadcrumbs = const <AppBreadcrumbItem>[],
this.actions,
this.toolbar,
required this.child,
});
final String title;
final String? subtitle;
final List<AppBreadcrumbItem> breadcrumbs;
final List<Widget>? actions;
final Widget? toolbar;
final Widget child;
@override
Widget build(BuildContext context) {
return SelectionArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (breadcrumbs.isNotEmpty) ...[
_BreadcrumbBar(items: breadcrumbs),
const SizedBox(height: 16),
],
PageHeader(title: title, subtitle: subtitle, actions: actions),
if (toolbar != null) ...[const SizedBox(height: 16), toolbar!],
const SizedBox(height: 24),
child,
],
),
),
);
}
}
class AppBreadcrumbItem {
const AppBreadcrumbItem({required this.label, this.path, this.onTap});
final String label;
final String? path;
final VoidCallback? onTap;
void navigate(BuildContext context) {
if (path != null && path!.isNotEmpty) {
context.go(path!);
return;
}
onTap?.call();
}
}
class _BreadcrumbBar extends StatelessWidget {
const _BreadcrumbBar({required this.items});
final List<AppBreadcrumbItem> items;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final colorScheme = theme.colorScheme;
return Wrap(
spacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (int index = 0; index < items.length; index++) ...[
if (index != 0)
Icon(
LucideIcons.chevronRight,
size: 14,
color: colorScheme.mutedForeground,
),
_BreadcrumbChip(
item: items[index],
isLast: index == items.length - 1,
),
],
],
);
}
}
class _BreadcrumbChip extends StatelessWidget {
const _BreadcrumbChip({required this.item, required this.isLast});
final AppBreadcrumbItem item;
final bool isLast;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final label = Text(
item.label,
style: theme.textTheme.small.copyWith(
color: isLast
? theme.colorScheme.foreground
: theme.colorScheme.mutedForeground,
),
);
if (isLast || (item.path == null && item.onTap == null)) {
return label;
}
return InkWell(
onTap: () => item.navigate(context),
borderRadius: BorderRadius.circular(6),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
child: label,
),
);
}
}