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 [], this.actions, this.toolbar, required this.child, }); final String title; final String? subtitle; final List breadcrumbs; final List? 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 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, ), ); } }