Auger/lib/widgets/app_shell.dart

251 lines
7 KiB
Dart

import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
Color contentBgColor(BuildContext context) {
final theme = Theme.of(context);
final h = HSLColor.fromColor(theme.colorScheme.border).hue;
final dark = theme.brightness == Brightness.dark;
return dark
? HSLColor.fromAHSL(1, h, 0.35, 0.13).toColor()
: HSLColor.fromAHSL(1, h, 0.30, 0.88).toColor();
}
// The main shell. replaces Scaffold in all pages.
class AugorShell extends StatelessWidget {
const AugorShell({
super.key,
required this.child,
required this.titleTag,
this.headerLeading = const [],
this.headerTrailing = const [],
this.statusLeft,
this.statusRight,
});
final Widget child;
final String titleTag;
final List<Widget> headerLeading;
final List<Widget> headerTrailing;
final Widget? statusLeft;
final Widget? statusRight;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final monoStyle = GoogleFonts.ibmPlexMono(
fontSize: 11,
height: 1,
fontWeight: FontWeight.w600,
color: theme.colorScheme.mutedForeground,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── header ──────────────────────────────────────────────────────────
Container(
decoration: BoxDecoration(
color: theme.colorScheme.background,
border: Border(bottom: BorderSide(color: theme.colorScheme.border, width: 1)),
),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.only(left: 12, top: 4, bottom: 4),
child: Row(
children: headerLeading,
),
),
const Spacer(),
...headerTrailing,
// title tag box
Container(
color: theme.colorScheme.border,
constraints: const BoxConstraints(minWidth: 100),
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 10),
child: RichText(
text: TextSpan(text: titleTag.toUpperCase(), style: monoStyle),
),
),
const SizedBox(width: 64), // mac traffic lights gap
],
),
),
),
// ── content ─────────────────────────────────────────────────────────
Expanded(
child: Stack(
fit: StackFit.expand,
children: [
ColoredBox(
color: contentBgColor(context),
child: child,
),
const Positioned.fill(
child: IgnorePointer(
child: CustomPaint(painter: _InsetShadowPainter()),
),
),
],
),
),
// ── footer / status bar ─────────────────────────────────────────────
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: theme.colorScheme.background,
border: Border(top: BorderSide(color: theme.colorScheme.border, width: 1)),
),
child: Row(
children: [
Expanded(
child: statusLeft ?? const SizedBox.shrink(),
),
Expanded(
child: Center(
child: _NavItems(),
),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: statusRight ?? const SizedBox.shrink(),
),
),
],
),
),
],
);
}
}
// shared footer nav — home and settings links
class _NavItems extends StatelessWidget {
@override
Widget build(BuildContext context) {
final location = GoRouterState.of(context).uri.toString();
final theme = Theme.of(context);
final monoStyle = GoogleFonts.ibmPlexMono(
fontSize: 11,
height: 1,
fontWeight: FontWeight.w600,
);
Widget navItem(String label, String path) {
final active = location == path || (path == '/' && location == '/');
return GestureDetector(
onTap: () => GoRouter.of(context).go(path),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: active ? theme.colorScheme.border : Colors.transparent,
borderRadius: BorderRadius.circular(3),
),
child: RichText(
text: TextSpan(
text: label,
style: monoStyle.copyWith(
color: active
? theme.colorScheme.foreground
: theme.colorScheme.mutedForeground,
),
),
),
),
);
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
navItem('Home', '/'),
const SizedBox(width: 2),
navItem('Settings', '/settings'),
],
);
}
}
// footer status text helper — use this for statusLeft / statusRight
class StatusText extends StatelessWidget {
const StatusText(this.text, {super.key});
final String text;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return RichText(
text: TextSpan(
text: text,
style: GoogleFonts.ibmPlexMono(
fontSize: 11,
height: 1,
fontWeight: FontWeight.w600,
color: theme.colorScheme.mutedForeground,
),
),
);
}
}
class _InsetShadowPainter extends CustomPainter {
const _InsetShadowPainter();
@override
void paint(Canvas canvas, Size size) {
const blur = 12.0;
final rect = Offset.zero & size;
canvas.save();
canvas.clipRect(rect);
final innerRect = rect.deflate(24);
final ringClip = Path()
..addRect(rect)
..addRect(innerRect)
..fillType = PathFillType.evenOdd;
canvas.clipPath(ringClip);
final path = Path()
..addRect(rect.inflate(blur * 2))
..addRect(rect)
..fillType = PathFillType.evenOdd;
final paint = Paint()
..color = const Color(0x55000000)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, blur);
canvas.drawPath(path, paint);
canvas.restore();
}
@override
bool shouldRepaint(covariant CustomPainter old) => false;
}