import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import '../about/internal/application/TextType.dart'; import '../utils/Colors.dart'; import 'AmbientBackground.dart'; import 'ChromeTone.dart'; import 'GlassSurface.dart'; import 'scene/SceneArt.dart'; import 'text/Text.dart'; /// The house layout. On the default ambient tone the screen floats on sage /// mist with frosted controls and ink type, and the content rises on a frosted /// sheet. Screens that carry a colour — a goal, a standing tier — or a banner /// get a tinted stage instead, with white type and smoky glass. class Sheet extends StatelessWidget { /// Small all-caps label rendered in the 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; /// Stage colour. Leaving it null (with no banner) gives the ambient tone. final Color? chrome; /// Rendered inside the chrome beneath the title — the standing strip. final Widget? banner; final bool scrollable; /// A painted landscape behind a tinted stage's header, in place of the /// flat gradient. final List? scene; final int sceneSeed; const Sheet({ super.key, required this.title, required this.child, this.eyebrow = "", this.onBack, this.action, this.chrome, this.banner, this.scrollable = true, this.scene, this.sceneSeed = 0, }); bool get _light => chrome == null && banner == null && scene == null; @override Widget build(BuildContext context) { final bool light = _light; final Color stage = chrome ?? colorPrimaryDark; final Color ink = light ? colorInk : colorWhite; final Color soft = light ? colorInkSoft : colorWhite.withValues(alpha: 0.5); final Widget header = SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.fromLTRB(16, 6, 16, 18), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ SizedBox( width: 46, child: onBack == null ? null : Align( alignment: Alignment.centerLeft, child: glassCircle( CupertinoIcons.arrow_left, onBack!, onDark: !light), ), ), Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ if (eyebrow.isNotEmpty) ...[ text( eyebrow.toUpperCase(), 9, TextType.Bold, color: soft, letterSpacing: 1.2, align: TextAlign.center, ), const SizedBox(height: 3), ], text( title.toUpperCase(), 14, TextType.Medium, color: ink, letterSpacing: 0.6, align: TextAlign.center, ), ], ), ), SizedBox( width: 46, child: action == null ? null : Align( alignment: Alignment.centerRight, child: action, ), ), ], ), if (banner != null) ...[ const SizedBox(height: 18), banner!, ], ], ), ), ); final Widget body = Expanded( child: Container( decoration: BoxDecoration( color: light ? colorFrost : colorPrimaryLight, borderRadius: const BorderRadius.only( topLeft: Radius.zero, topRight: Radius.zero, ), ), 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, ), ), ); final Widget column = ChromeTone( light: light, child: Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.stretch, children: [header, body], ), ); if (light) { return Scaffold( backgroundColor: colorAmbientMid, body: AmbientBackground(child: column), ); } // A tinted stage: the colour deepens downward and a soft light blooms in // the corner, so it reads as lit rather than painted flat. return Scaffold( backgroundColor: stage, body: Stack( children: [ Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Color.lerp(stage, colorWhite, 0.14)!, stage, Color.lerp(stage, colorBlack, 0.35)!, ], ), ), ), ), if (scene != null) Positioned( top: 0, left: 0, right: 0, height: 250, child: ShaderMask( shaderCallback: (Rect bounds) => const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFF000000), Color(0x00000000)], stops: [0.6, 1.0], ).createShader(bounds), blendMode: BlendMode.dstIn, child: Stack( fit: StackFit.expand, children: [ SceneArt(palette: scene!, seed: sceneSeed), ColoredBox(color: colorBlack.withValues(alpha: 0.42)), ], ), ), ), Positioned( top: -120, right: -90, child: Container( width: 300, height: 300, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ colorWhite.withValues(alpha: 0.16), colorWhite.withValues(alpha: 0.0), ], ), ), ), ), Positioned.fill(child: column), ], ), ); } } /// A round frosted control for the chrome, with an optional unread dot. It /// matches the stage it sits on. Widget chromeAction( IconData icon, VoidCallback onTap, { bool dotted = false, Color? dotColor, }) { return Builder( builder: (BuildContext context) => glassCircle( icon, onTap, onDark: !ChromeTone.isLight(context), dotted: dotted, dotColor: dotColor, ), ); } /// 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 ?? colorInkSoft, letterSpacing: 1.0, ), const SizedBox(height: 5), text(value, valueSize, valueType, color: valueColor ?? colorInk), ], ); } /// 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 ?? colorInk, height: 1.15); } /// A section heading: medium ink label, a soft caption beside it and an /// optional trailing control ("View all"). Widget sectionBreak(String heading, {Widget? trailing, String caption = ""}) { return Padding( padding: const EdgeInsets.only(top: 6, bottom: 14), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ text(heading, 16, TextType.Medium, color: colorInk), if (caption.isNotEmpty) ...[ const SizedBox(width: 8), Flexible( child: Padding( padding: const EdgeInsets.only(bottom: 1), child: text(caption, 11, TextType.Regular, color: colorInkSoft, maxLines: 1, overflow: TextOverflow.ellipsis), ), ), ], ], ), ), if (trailing != null) trailing, ], ), ); }