Redesign the UI after the fitness reference and replace the logo
Light sage-to-cream ambient backdrop, frosted glass controls and painted SceneArt landscapes in place of photography. Home becomes a dashboard with a two-tone greeting, a frosted summary ring, category portraits, an up-next rail of image-led cards and a floating glass tab bar. The routine library gets an image header and painted cards; the player gets a scene stage, steps on an arc, a dotted clock and a night pill with a lime spark. Goals become painted cards in their own colour. New logo: a lime sun resting on a ground line under two arcs. The splash raises the sun and lights the arcs; launcher icons and native splash are regenerated to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
@@ -1,15 +1,20 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../about/internal/application/TextType.dart';
|
||||
import '../utils/Colors.dart';
|
||||
import 'Component.dart';
|
||||
import 'AmbientBackground.dart';
|
||||
import 'ChromeTone.dart';
|
||||
import 'GlassSurface.dart';
|
||||
import 'scene/SceneArt.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.
|
||||
/// 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 black chrome, above the title.
|
||||
/// Small all-caps label rendered in the chrome, above the title.
|
||||
final String eyebrow;
|
||||
|
||||
/// The chrome title — small and centred, not the display title.
|
||||
@@ -22,7 +27,7 @@ class Sheet extends StatelessWidget {
|
||||
/// Optional trailing control in the chrome.
|
||||
final Widget? action;
|
||||
|
||||
/// Chrome colour. Defaults to near-black; standing screens tint it.
|
||||
/// 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.
|
||||
@@ -30,6 +35,12 @@ class Sheet extends StatelessWidget {
|
||||
|
||||
final bool scrollable;
|
||||
|
||||
/// A painted landscape behind a tinted stage's header, in place of the
|
||||
/// flat gradient.
|
||||
final List<Color>? scene;
|
||||
|
||||
final int sceneSeed;
|
||||
|
||||
const Sheet({
|
||||
super.key,
|
||||
required this.title,
|
||||
@@ -40,158 +51,207 @@ class Sheet extends StatelessWidget {
|
||||
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 Color chromeColor = chrome ?? colorPrimaryDark;
|
||||
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);
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
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),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 44,
|
||||
child: action == null
|
||||
? null
|
||||
: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: action,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
SizedBox(
|
||||
width: 46,
|
||||
child: action == null
|
||||
? null
|
||||
: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: action,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (banner != null) ...[
|
||||
const SizedBox(height: 18),
|
||||
banner!,
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _chromeButton(IconData icon, VoidCallback onTap) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
final Widget body = Expanded(
|
||||
child: Container(
|
||||
width: 38,
|
||||
height: 38,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: colorWhite.withValues(alpha: 0.10),
|
||||
shape: BoxShape.circle,
|
||||
color: light ? colorFrost : colorPrimaryLight,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(32),
|
||||
topRight: Radius.circular(32),
|
||||
),
|
||||
),
|
||||
child: Icon(icon, size: 15, color: colorWhite),
|
||||
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>[
|
||||
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>[Color(0xFF000000), Color(0x00000000)],
|
||||
stops: <double>[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: <Color>[
|
||||
colorWhite.withValues(alpha: 0.16),
|
||||
colorWhite.withValues(alpha: 0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(child: column),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A circular control for the black chrome, with an optional unread dot.
|
||||
/// 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 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
return Builder(
|
||||
builder: (BuildContext context) => glassCircle(
|
||||
icon,
|
||||
onTap,
|
||||
onDark: !ChromeTone.isLight(context),
|
||||
dotted: dotted,
|
||||
dotColor: dotColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -215,12 +275,11 @@ Widget labelled(
|
||||
label.toUpperCase(),
|
||||
9,
|
||||
TextType.Bold,
|
||||
color: labelColor ?? colorGrey2,
|
||||
color: labelColor ?? colorInkSoft,
|
||||
letterSpacing: 1.0,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
text(value, valueSize, valueType,
|
||||
color: valueColor ?? colorPrimaryDark),
|
||||
text(value, valueSize, valueType, color: valueColor ?? colorInk),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -247,41 +306,40 @@ Widget metaRow(List<Widget> items) {
|
||||
/// 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);
|
||||
color: color ?? colorInk, height: 1.15);
|
||||
}
|
||||
|
||||
/// A section break: hairline, then a small bold heading with an optional
|
||||
/// trailing chip.
|
||||
/// 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 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),
|
||||
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: colorGrey2),
|
||||
color: colorInkSoft,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (trailing != null) trailing,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
if (trailing != null) trailing,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user