import 'package:flutter/material.dart'; import '../about/internal/application/TextType.dart'; import '../utils/Colors.dart'; import 'Component.dart'; import 'text/Text.dart'; /// The house layout: black chrome at the top, a white sheet rising into it with /// a large corner radius. Every screen is built from this so the app reads as /// one object rather than a stack of pages. class Sheet extends StatelessWidget { /// Small all-caps label rendered in the black chrome, above the title. final String eyebrow; /// The chrome title — small and centred, not the display title. final String title; final Widget child; final VoidCallback? onBack; /// Optional trailing control in the chrome. final Widget? action; /// Chrome colour. Defaults to near-black; standing screens tint it. final Color? chrome; /// Rendered inside the chrome beneath the title — the standing strip. final Widget? banner; final bool scrollable; const Sheet({ super.key, required this.title, required this.child, this.eyebrow = "", this.onBack, this.action, this.chrome, this.banner, this.scrollable = true, }); @override Widget build(BuildContext context) { final Color chromeColor = chrome ?? colorPrimaryDark; return Scaffold( backgroundColor: chromeColor, body: Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.fromLTRB(12, 4, 12, 16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ SizedBox( width: 44, child: onBack == null ? null : _chromeButton( Icons.arrow_back_ios_new_rounded, onBack!), ), Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ if (eyebrow.isNotEmpty) ...[ text( eyebrow.toUpperCase(), 9, TextType.Bold, color: colorWhite.withValues(alpha: 0.45), letterSpacing: 1.2, align: TextAlign.center, ), const SizedBox(height: 3), ], text( title, 15, TextType.Medium, color: colorWhite, align: TextAlign.center, ), ], ), ), SizedBox( width: 44, child: action == null ? null : Align( alignment: Alignment.centerRight, child: action, ), ), ], ), if (banner != null) ...[ const SizedBox(height: 16), banner!, ], ], ), ), ), Expanded( child: Container( decoration: BoxDecoration( color: colorPrimaryLight, borderRadius: const BorderRadius.only( topLeft: Radius.circular(28), topRight: Radius.circular(28), ), ), clipBehavior: Clip.antiAlias, child: scrollable ? SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 28, 20, 40), child: child, ) : Padding( padding: const EdgeInsets.fromLTRB(20, 28, 20, 0), child: child, ), ), ), ], ), ); } Widget _chromeButton(IconData icon, VoidCallback onTap) { return GestureDetector( onTap: onTap, child: Container( width: 38, height: 38, alignment: Alignment.center, decoration: BoxDecoration( color: colorWhite.withValues(alpha: 0.10), shape: BoxShape.circle, ), child: Icon(icon, size: 15, color: colorWhite), ), ); } } /// A circular control for the black chrome, with an optional unread dot. Widget chromeAction( IconData icon, VoidCallback onTap, { bool dotted = false, Color? dotColor, }) { return GestureDetector( onTap: onTap, child: Stack( clipBehavior: Clip.none, children: [ Container( width: 38, height: 38, alignment: Alignment.center, decoration: BoxDecoration( color: colorWhite.withValues(alpha: 0.10), shape: BoxShape.circle, ), child: Icon(icon, size: 17, color: colorWhite), ), if (dotted) Positioned( top: 1, right: 1, child: Container( width: 9, height: 9, decoration: BoxDecoration( color: dotColor ?? colorAccent, shape: BoxShape.circle, border: Border.all(color: colorPrimaryDark, width: 1.5), ), ), ), ], ), ); } /// The label/value pair — a tiny grey all-caps label sitting directly above a /// value. The core unit of the whole interface. Widget labelled( String label, String value, { double valueSize = 15, TextType valueType = TextType.Medium, Color? valueColor, Color? labelColor, CrossAxisAlignment align = CrossAxisAlignment.start, }) { return Column( crossAxisAlignment: align, mainAxisSize: MainAxisSize.min, children: [ text( label.toUpperCase(), 9, TextType.Bold, color: labelColor ?? colorGrey2, letterSpacing: 1.0, ), const SizedBox(height: 5), text(value, valueSize, valueType, color: valueColor ?? colorPrimaryDark), ], ); } /// A row of metadata above a display title, as on the reference: small grey /// pairs separated by generous space. Widget metaRow(List items) { final List spaced = []; for (int index = 0; index < items.length; index++) { spaced.add(items[index]); if (index != items.length - 1) { spaced.add(const SizedBox(width: 28)); } } return Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: spaced, ); } /// The oversized light display title that opens a section. Widget displayTitle(String value, {Color? color, double size = 34}) { return text(value, size, TextType.Light, color: color ?? colorPrimaryDark, height: 1.15); } /// A section break: hairline, then a small bold heading with an optional /// trailing chip. Widget sectionBreak(String heading, {Widget? trailing, String caption = ""}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ hairline(margin: const EdgeInsets.only(bottom: 20)), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ text(heading, 17, TextType.Bold, color: colorPrimaryDark), if (caption.isNotEmpty) ...[ const SizedBox(width: 10), Padding( padding: const EdgeInsets.only(bottom: 2), child: text(caption, 11, TextType.Regular, color: colorGrey2), ), ], ], ), ), if (trailing != null) trailing, ], ), const SizedBox(height: 16), ], ); }