import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../core/theme.dart'; /// Small uppercase section heading used above every group of cards. class SectionLabel extends StatelessWidget { const SectionLabel(this.text, {super.key, this.trailing}); final String text; final Widget? trailing; @override Widget build(BuildContext context) { // No horizontal inset: the label has to share a left edge with the cards // and body copy it sits above. return Padding( padding: const EdgeInsets.fromLTRB(0, 22, 0, 10), child: Row( children: [ Expanded( child: Text( text.toUpperCase(), style: const TextStyle( fontSize: 10.5, fontWeight: FontWeight.w600, letterSpacing: 1.4, color: Color(0xFF949FA5), ), ), ), ?trailing, ], ), ); } } enum ChipTone { blue, yellow, neutral, muted, danger } /// Status pill. Yellow marks anything waiting on the worker. class StatusChip extends StatelessWidget { const StatusChip( this.label, { super.key, this.tone = ChipTone.neutral, this.icon, }); final String label; final ChipTone tone; final IconData? icon; @override Widget build(BuildContext context) { final (bg, fg) = switch (tone) { ChipTone.blue => (KrowPalette.blueTint, KrowPalette.blue), ChipTone.yellow => (KrowPalette.yellow, KrowPalette.ink), ChipTone.neutral => (const Color(0xFFDEEBE2), KrowPalette.ink), ChipTone.muted => (const Color(0xFFDEEBE2), const Color(0xFF65727A)), ChipTone.danger => (const Color(0xFFFBE3E3), const Color(0xFFA6403F)), }; return Container( padding: EdgeInsets.fromLTRB(icon == null ? 10 : 8, 6, 10, 6), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(9), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon(icon, size: 13, color: fg), const SizedBox(width: 5), ], // Flexible so the chip gives way when the row it sits in runs out of // width — at a large text scale it otherwise overflows its parent. Flexible( child: Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11.5, fontWeight: FontWeight.w600, color: fg, ), ), ), ], ), ); } } /// Soft informational panel. `warning: true` switches it to the yellow tone. class InfoBanner extends StatelessWidget { const InfoBanner(this.text, {super.key, this.warning = false, this.icon}); final String text; final bool warning; final IconData? icon; @override Widget build(BuildContext context) { final bg = warning ? const Color(0xFFF7E4AE) : KrowPalette.blueTint; final fg = warning ? KrowPalette.ink : KrowPalette.blueDeep; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(12), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( icon ?? (warning ? LucideIcons.triangleAlert : LucideIcons.info), size: 16, color: fg, ), const SizedBox(width: 11), Expanded( child: Text( text, style: TextStyle(fontSize: 12.5, height: 1.5, color: fg), ), ), ], ), ); } } /// White rounded container used for every grouped block. class KrowCard extends StatelessWidget { const KrowCard({ super.key, required this.child, this.padding = const EdgeInsets.all(16), this.onTap, this.dashed = false, }); final Widget child; final EdgeInsetsGeometry padding; final VoidCallback? onTap; final bool dashed; @override Widget build(BuildContext context) { return Material( color: dashed ? Colors.transparent : Colors.white, borderRadius: BorderRadius.circular(16), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Container( padding: padding, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), border: Border.all(color: KrowPalette.line), ), child: child, ), ), ); } } /// A navigation row: icon tile, title, subtitle, optional badge, chevron. class NavTile extends StatelessWidget { const NavTile({ super.key, required this.icon, required this.title, this.subtitle, this.badge, this.onTap, }); final IconData icon; final String title; final String? subtitle; final Widget? badge; final VoidCallback? onTap; @override Widget build(BuildContext context) { return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(vertical: 14), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: KrowPalette.blueTint, borderRadius: BorderRadius.circular(11), ), child: Icon(icon, size: 18, color: KrowPalette.blue), ), const SizedBox(width: 13), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 14.5, fontWeight: FontWeight.w600, ), ), if (subtitle != null) Padding( padding: const EdgeInsets.only(top: 2), child: Text( subtitle!, style: const TextStyle( fontSize: 12, height: 1.35, color: KrowPalette.mute, ), ), ), ], ), ), if (badge != null) ...[const SizedBox(width: 8), badge!], const SizedBox(width: 8), const Icon( LucideIcons.chevronRight, size: 17, color: Color(0xFF949FA5), ), ], ), ), ); } } /// Groups NavTiles into a single card with hairline dividers between rows. class TileGroup extends StatelessWidget { const TileGroup({super.key, required this.children}); final List children; @override Widget build(BuildContext context) { return KrowCard( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ for (var i = 0; i < children.length; i++) ...[ if (i > 0) const Divider(height: 1), children[i], ], ], ), ); } } /// Dotted-leader row: label on the left, value on the right. class DetailRow extends StatelessWidget { const DetailRow( this.label, this.value, { super.key, this.emphasis = false, this.valueColor, }); final String label; final String value; final bool emphasis; final Color? valueColor; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 11), child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Expanded( child: Text( label, style: TextStyle( fontSize: 13.5, fontWeight: emphasis ? FontWeight.w600 : FontWeight.w400, ), ), ), const SizedBox(width: 12), Text( value, style: TextStyle( fontSize: emphasis ? 16 : 13.5, fontWeight: FontWeight.w700, letterSpacing: -0.3, color: valueColor ?? KrowPalette.ink, ), ), ], ), ); } } /// Three-up figure block: caption over a big number. /// /// Both lines are pinned to a fixed height and scale down instead of wrapping. /// A wrapping caption or figure — "CLOCK OUT", "11:50 PM" — would make one /// card taller than the two beside it and leave the row ragged. class StatSlab extends StatelessWidget { const StatSlab(this.caption, this.value, {super.key, this.valueColor}); final String caption; final String value; final Color? valueColor; static const _captionHeight = 13.0; static const _valueHeight = 25.0; @override Widget build(BuildContext context) { return KrowCard( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ SizedBox( height: _captionHeight, child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text( caption.toUpperCase(), maxLines: 1, style: const TextStyle( fontSize: 9, fontWeight: FontWeight.w600, letterSpacing: 1.2, color: Color(0xFF949FA5), ), ), ), ), const SizedBox(height: 6), SizedBox( height: _valueHeight, child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text( value, maxLines: 1, style: TextStyle( fontSize: 17.5, fontWeight: FontWeight.w700, letterSpacing: -0.6, color: valueColor ?? KrowPalette.ink, ), ), ), ), ], ), ); } } /// Lays out slabs as an evenly spaced row of equal-height cards. class StatSlabRow extends StatelessWidget { const StatSlabRow({super.key, required this.slabs}); final List slabs; @override Widget build(BuildContext context) { // These rows live inside scrollables, where the height is unbounded, so // `stretch` alone would try to fill infinity. IntrinsicHeight gives the // row the height of its tallest card first, and stretch then matches the // others to it. return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < slabs.length; i++) ...[ if (i > 0) const SizedBox(width: 9), Expanded(child: slabs[i]), ], ], ), ); } } /// Horizontal rail of small stats, divided by hairlines. class StatRail extends StatelessWidget { const StatRail({super.key, required this.stats}); final List<(String, String)> stats; @override Widget build(BuildContext context) { return KrowCard( padding: EdgeInsets.zero, child: IntrinsicHeight( child: Row( children: [ for (var i = 0; i < stats.length; i++) ...[ if (i > 0) const VerticalDivider(width: 1), Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 14), child: Column( children: [ Text( stats[i].$2, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, letterSpacing: -0.4, ), ), const SizedBox(height: 4), Text( stats[i].$1.toUpperCase(), textAlign: TextAlign.center, style: const TextStyle( fontSize: 8.5, fontWeight: FontWeight.w600, letterSpacing: 0.6, color: Color(0xFF949FA5), ), ), ], ), ), ), ], ], ), ), ); } } /// Progress that grows into place rather than snapping, so a change — an /// uploaded document, a day worked — is something you see happen. class KrowProgressBar extends StatelessWidget { const KrowProgressBar(this.value, {super.key, this.color}); final double value; final Color? color; @override Widget build(BuildContext context) { return TweenAnimationBuilder( tween: Tween(end: value.clamp(0, 1)), duration: const Duration(milliseconds: 600), curve: Curves.easeOutCubic, builder: (context, animated, _) => ClipRRect( borderRadius: BorderRadius.circular(3), child: LinearProgressIndicator( value: animated, minHeight: 6, backgroundColor: KrowPalette.line, valueColor: AlwaysStoppedAnimation(color ?? KrowPalette.blue), ), ), ); } } class EmptyState extends StatelessWidget { const EmptyState({ super.key, required this.icon, required this.title, required this.message, this.action, }); final IconData icon; final String title; final String message; final Widget? action; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 34), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), border: Border.all(color: KrowPalette.line, style: BorderStyle.solid), ), child: Column( children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: KrowPalette.line), ), child: Icon(icon, size: 24, color: const Color(0xFF949FA5)), ), const SizedBox(height: 14), Text( title, textAlign: TextAlign.center, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700), ), const SizedBox(height: 5), Text( message, textAlign: TextAlign.center, style: const TextStyle( fontSize: 13, height: 1.5, color: KrowPalette.mute, ), ), if (action != null) ...[const SizedBox(height: 16), action!], ], ), ); } } /// Business identity tile used on every shift card. class BusinessBadge extends StatelessWidget { const BusinessBadge(this.letter, {super.key, this.size = 46}); final String letter; final double size; @override Widget build(BuildContext context) { return Container( width: size, height: size, alignment: Alignment.center, decoration: BoxDecoration( color: KrowPalette.blue, borderRadius: BorderRadius.circular(size / 3.2), ), child: Text( letter, style: TextStyle( fontSize: size * 0.42, fontWeight: FontWeight.w800, color: Colors.white, ), ), ); } } /// Small icon + text pair used for shift metadata. class MetaPill extends StatelessWidget { const MetaPill(this.icon, this.text, {super.key}); final IconData icon; final String text; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: const Color(0x0F26353E), borderRadius: BorderRadius.circular(9), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 12, color: const Color(0xFF949FA5)), const SizedBox(width: 6), Flexible( child: Text( text, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 11.5, fontWeight: FontWeight.w600, ), ), ), ], ), ); } } /// The blue ground every header sits on: brand colour, rounded bottom, and two /// soft glows. The glows are quiet on their own, but they give the frosted /// controls sitting on top something to refract. class BrandSurface extends StatelessWidget { const BrandSurface({super.key, required this.child}); final Widget child; @override Widget build(BuildContext context) { return ClipRRect( borderRadius: const BorderRadius.vertical(bottom: Radius.circular(16)), child: Container( width: double.infinity, color: KrowPalette.blue, child: Stack( children: [ Positioned( top: -70, right: -40, child: _Glow(220, Colors.white.withValues(alpha: 0.16)), ), Positioned( bottom: -90, left: -50, child: _Glow(240, KrowPalette.yellow.withValues(alpha: 0.14)), ), child, ], ), ), ); } } /// The blue header block that tops a pushed detail screen. Main tabs use /// KrowSliverHeader instead, so their header stays put while content scrolls. class BrandHeader extends StatelessWidget { const BrandHeader({ super.key, required this.child, this.padding = const EdgeInsets.fromLTRB(20, 8, 20, 24), }); final Widget child; final EdgeInsetsGeometry padding; @override Widget build(BuildContext context) { return BrandSurface( child: SafeArea( bottom: false, child: Padding(padding: padding, child: child), ), ); } } /// A soft circular bloom. Drawn as a radial gradient rather than a blurred /// shape, which costs nothing per frame. class _Glow extends StatelessWidget { const _Glow(this.size, this.color); final double size; final Color color; @override Widget build(BuildContext context) { return IgnorePointer( child: Container( width: size, height: size, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient(colors: [color, color.withValues(alpha: 0)]), ), ), ); } } /// Round icon button used in headers, on light or dark ground. class HeaderIconButton extends StatelessWidget { const HeaderIconButton({ super.key, required this.icon, required this.onPressed, this.onDark = true, this.tooltip, }); final IconData icon; final VoidCallback onPressed; final bool onDark; final String? tooltip; @override Widget build(BuildContext context) { return Material( color: onDark ? Colors.white24 : Colors.white, borderRadius: BorderRadius.circular(11), child: InkWell( onTap: onPressed, borderRadius: BorderRadius.circular(11), child: Tooltip( message: tooltip ?? '', child: Container( width: 40, height: 40, decoration: BoxDecoration( borderRadius: BorderRadius.circular(11), border: onDark ? null : Border.all(color: KrowPalette.line), ), child: Icon( icon, size: 19, color: onDark ? Colors.white : KrowPalette.ink, ), ), ), ), ); } } /// Simple bar chart used on the earnings screens. class MiniBarChart extends StatelessWidget { const MiniBarChart({super.key, required this.bars}); /// `(label, value, isPaid)` — paid bars read blue, pending read yellow. final List<(String, double, bool)> bars; @override Widget build(BuildContext context) { final max = bars.fold(0, (m, b) => b.$2 > m ? b.$2 : m); return Column( children: [ SizedBox( height: 118, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ for (final bar in bars) Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6), // Bars grow to their new height when the range changes, // so you can see which column moved. child: TweenAnimationBuilder( tween: Tween( end: max == 0 ? 0.0 : (bar.$2 / max).clamp(0.0, 1.0).toDouble(), ), duration: const Duration(milliseconds: 520), curve: Curves.easeOutCubic, builder: (context, height, _) => FractionallySizedBox( heightFactor: height, child: TweenAnimationBuilder( tween: ColorTween( end: bar.$3 ? KrowPalette.blue : KrowPalette.yellow, ), duration: const Duration(milliseconds: 320), builder: (context, color, _) => Container( decoration: BoxDecoration( color: color, borderRadius: const BorderRadius.vertical( top: Radius.circular(6), bottom: Radius.circular(3), ), ), ), ), ), ), ), ), ], ), ), const SizedBox(height: 9), Row( children: [ for (final bar in bars) Expanded( child: Text( bar.$1, textAlign: TextAlign.center, style: const TextStyle( fontSize: 9.5, fontWeight: FontWeight.w500, color: Color(0xFF949FA5), ), ), ), ], ), ], ); } } /// Legend dot + label, paired with the chart. class LegendDot extends StatelessWidget { const LegendDot(this.color, this.label, {super.key}); final Color color; final String label; @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 6), Text( label, style: const TextStyle(fontSize: 12, color: KrowPalette.mute), ), ], ); } }