251 lines
7 KiB
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;
|
|
}
|