tasq/lib/widgets/app_page_header.dart

102 lines
3.0 KiB
Dart

import 'package:flutter/material.dart';
import '../theme/m3_motion.dart';
/// Standardized M3 Expressive page header with animated entrance.
///
/// Replaces the ad-hoc `Padding(Align(Text(...)))` pattern found across
/// screens. Provides:
/// * `headlineSmall` typography — correct M3 size for a top-level page title.
/// * [M3FadeSlideIn] entrance animation — 400 ms with emphasized easing.
/// * Optional [subtitle] in `bodyMedium` / `onSurfaceVariant`.
/// * Optional [actions] row rendered at the trailing end.
/// * Responsive alignment: left-aligned on desktop (≥600 dp), centered on
/// mobile to match the existing navigation-bar-centric layout.
class AppPageHeader extends StatelessWidget {
const AppPageHeader({
super.key,
required this.title,
this.subtitle,
this.actions,
this.padding = const EdgeInsets.only(top: 20, bottom: 12),
});
final String title;
final String? subtitle;
/// Optional trailing action widgets (e.g. filter chip, icon button).
final List<Widget>? actions;
/// Vertical padding around the header. Horizontal padding is handled by
/// the parent [ResponsiveBody] so only top/bottom should be set here.
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
final tt = Theme.of(context).textTheme;
final cs = Theme.of(context).colorScheme;
final titleWidget = Text(
title,
style: tt.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: -0.3,
color: cs.onSurface,
),
textAlign: TextAlign.center,
);
final subtitleWidget = subtitle == null
? null
: Text(
subtitle!,
style: tt.bodyMedium?.copyWith(color: cs.onSurfaceVariant),
textAlign: TextAlign.center,
);
final hasActions = actions != null && actions!.isNotEmpty;
Widget content;
if (!hasActions) {
content = Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
titleWidget,
if (subtitleWidget != null) ...[
const SizedBox(height: 4),
subtitleWidget,
],
],
);
} else {
// With actions — centered title/subtitle, actions sit to the right.
content = Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
titleWidget,
if (subtitleWidget != null) ...[
const SizedBox(height: 4),
subtitleWidget,
],
],
),
),
const SizedBox(width: 8),
Row(mainAxisSize: MainAxisSize.min, children: actions!),
],
);
}
return M3FadeSlideIn(
duration: M3Motion.standard,
child: Padding(padding: padding, child: content),
);
}
}