2026-10-28 23:18:03 +01:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
|
|
|
|
import 'zen_style.dart';
|
|
|
|
|
|
|
|
|
|
/// A tab's big title and an optional line under it.
|
|
|
|
|
class PageHeader extends StatelessWidget {
|
2026-10-28 23:48:07 +01:00
|
|
|
const PageHeader({super.key, required this.title, this.subtitle, this.action});
|
2026-10-28 23:18:03 +01:00
|
|
|
|
|
|
|
|
final String title;
|
|
|
|
|
final String? subtitle;
|
|
|
|
|
|
2026-10-28 23:48:07 +01:00
|
|
|
/// A button beside the title, such as "New plan".
|
|
|
|
|
final Widget? action;
|
|
|
|
|
|
2026-10-28 23:18:03 +01:00
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final s = ZenStyle.of(context);
|
|
|
|
|
final text = Theme.of(context).textTheme;
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 28, 20, 16),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
2026-10-28 23:48:07 +01:00
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(child: Text(title, style: text.headlineLarge)),
|
|
|
|
|
if (action != null) ...[const SizedBox(width: 12), action!],
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-10-28 23:18:03 +01:00
|
|
|
if (subtitle != null) ...[
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
Text(subtitle!, style: text.bodyLarge?.copyWith(color: s.inkMuted)),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// What an empty tab shows: what will appear here and how to get it there.
|
|
|
|
|
class EmptyState extends StatelessWidget {
|
|
|
|
|
const EmptyState({super.key, required this.icon, required this.title, required this.body});
|
|
|
|
|
|
|
|
|
|
final IconData icon;
|
|
|
|
|
final String title;
|
|
|
|
|
final String body;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final s = ZenStyle.of(context);
|
|
|
|
|
final text = Theme.of(context).textTheme;
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
width: 56,
|
|
|
|
|
height: 56,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: s.surfaceSoft,
|
|
|
|
|
borderRadius: BorderRadius.circular(s.controlRadius.clamp(0, 28)),
|
|
|
|
|
),
|
|
|
|
|
child: Icon(icon, color: s.accent),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
Text(title, style: text.titleLarge),
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
ConstrainedBox(
|
|
|
|
|
constraints: const BoxConstraints(maxWidth: 340),
|
|
|
|
|
child: Text(body, style: text.bodyMedium?.copyWith(color: s.inkMuted)),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|