Switch to Inter, round the corners softly and make the nav switch tabs
Inter replaces General Sans (Inter Display for titles and big numbers). Corners come back from before the squaring commit, scaled to a soft set (16 cards, 14 buttons, 12 rows, 8 chips) and applied to the screens built since. The nav is now a cluster of round frosted buttons that switches Today, Routines, Goals, Groups and Progress in place inside Home: no pushed pages, no back arrows, and Android back returns to Today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
@@ -48,7 +48,7 @@ class TickBox extends StatelessWidget {
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: checked ? tone : Colors.transparent,
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: checked ? tone : colorGrey.withValues(alpha: 0.55),
|
||||
width: 1.6,
|
||||
@@ -202,7 +202,7 @@ Widget addTaskAffordance(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: tone.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: tone.withValues(alpha: 0.25), width: 1),
|
||||
),
|
||||
child: Icon(Icons.add_rounded, size: 22, color: tone),
|
||||
|
||||
@@ -5,7 +5,7 @@ import '../utils/Colors.dart';
|
||||
import 'text/Text.dart';
|
||||
|
||||
/// The font families, wrapped so no screen ever names a family directly.
|
||||
/// General Sans — see fonts/LICENSE-GeneralSans.txt.
|
||||
/// Inter (SIL OFL) — see fonts/LICENSE-Inter.txt.
|
||||
String getTextType(TextType type) {
|
||||
switch (type) {
|
||||
case TextType.Bold:
|
||||
@@ -32,7 +32,7 @@ Widget pill(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: background,
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: foreground.withValues(alpha: 0.20), width: 1),
|
||||
),
|
||||
child: Row(
|
||||
@@ -61,7 +61,7 @@ Widget card({
|
||||
EdgeInsets margin = EdgeInsets.zero,
|
||||
Color? background,
|
||||
Color? borderColor,
|
||||
double radius = 22,
|
||||
double radius = 16,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
final Widget body = Container(
|
||||
@@ -70,7 +70,7 @@ Widget card({
|
||||
margin: margin,
|
||||
decoration: BoxDecoration(
|
||||
color: background ?? colorCard,
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
border: Border.all(color: borderColor ?? colorGlassBorder, width: 1),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
@@ -159,6 +159,7 @@ Widget emptyState(
|
||||
height: 76,
|
||||
decoration: BoxDecoration(
|
||||
color: tone.withValues(alpha: 0.08),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 32, color: tone),
|
||||
),
|
||||
@@ -190,7 +191,7 @@ Widget meter(
|
||||
: fraction;
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: LinearProgressIndicator(
|
||||
value: clamped,
|
||||
minHeight: height,
|
||||
|
||||
@@ -1,125 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../about/internal/application/TextType.dart';
|
||||
import '../utils/Colors.dart';
|
||||
import 'GlassSurface.dart';
|
||||
import 'text/Text.dart';
|
||||
|
||||
typedef NavEntry = ({IconData icon, String label, VoidCallback onTap});
|
||||
|
||||
/// The floating tab bar: the selected tab is a filled night block, the rest
|
||||
/// sit quietly on white. An optional dark action button can sit beside it.
|
||||
/// The floating tab bar: a cluster of round frosted buttons, icons only.
|
||||
/// The open tab rises out of the row as a solid white disc. It switches tabs
|
||||
/// in place; nothing here pushes a page.
|
||||
class FloatingNav extends StatelessWidget {
|
||||
final List<NavEntry> entries;
|
||||
|
||||
final int selected;
|
||||
|
||||
final IconData? actionIcon;
|
||||
|
||||
final VoidCallback? onAction;
|
||||
|
||||
const FloatingNav({
|
||||
super.key,
|
||||
required this.entries,
|
||||
required this.selected,
|
||||
this.actionIcon,
|
||||
this.onAction,
|
||||
});
|
||||
|
||||
static const double _size = 58;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.zero,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x1A1A1F1C),
|
||||
blurRadius: 30,
|
||||
offset: Offset(0, 10),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(_size)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color(0x1F1A1F1C),
|
||||
blurRadius: 30,
|
||||
offset: Offset(0, 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: GlassSurface(
|
||||
radius: const BorderRadius.all(Radius.circular(_size)),
|
||||
tint: colorGlass,
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int index = 0; index < entries.length; index++) ...[
|
||||
if (index > 0) const SizedBox(width: 4),
|
||||
_button(entries[index], index == selected),
|
||||
],
|
||||
),
|
||||
child: GlassSurface(
|
||||
radius: BorderRadius.zero,
|
||||
tint: colorGlassStrong,
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Row(
|
||||
children: [
|
||||
for (int index = 0; index < entries.length; index++)
|
||||
Expanded(child: _tab(entries[index], index == selected)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (onAction != null) ...[
|
||||
const SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: onAction,
|
||||
child: Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const RadialGradient(
|
||||
center: Alignment(-0.3, -0.4),
|
||||
colors: <Color>[colorNightGlow, colorNight],
|
||||
),
|
||||
border: Border.all(color: colorWhite, width: 3),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x331A1F1C),
|
||||
blurRadius: 24,
|
||||
offset: Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(actionIcon, size: 24, color: colorLime),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tab(NavEntry entry, bool active) {
|
||||
return GestureDetector(
|
||||
onTap: entry.onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? colorHeaderBottom : colorWhite.withValues(alpha: 0.0),
|
||||
borderRadius: BorderRadius.zero,
|
||||
boxShadow: active
|
||||
? const [
|
||||
BoxShadow(
|
||||
color: Color(0x141A1F1C),
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: const [],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(entry.icon,
|
||||
size: 23, color: active ? colorWhite : colorInkSoft),
|
||||
const SizedBox(height: 4),
|
||||
text(entry.label, 10, active ? TextType.Medium : TextType.Regular,
|
||||
color: active ? colorWhite : colorInkSoft, maxLines: 1),
|
||||
],
|
||||
Widget _button(NavEntry entry, bool active) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: active,
|
||||
label: entry.label,
|
||||
child: Tooltip(
|
||||
message: entry.label,
|
||||
child: GestureDetector(
|
||||
onTap: entry.onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
width: _size,
|
||||
height: _size,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: active ? colorWhite : colorWhite.withValues(alpha: 0.35),
|
||||
border: Border.all(
|
||||
color: colorWhite.withValues(alpha: active ? 1 : 0.6),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: active
|
||||
? const [
|
||||
BoxShadow(
|
||||
color: Color(0x261A1F1C),
|
||||
blurRadius: 16,
|
||||
offset: Offset(0, 6),
|
||||
),
|
||||
]
|
||||
: const [],
|
||||
),
|
||||
child: Icon(entry.icon,
|
||||
size: 22, color: active ? colorInk : colorInkSoft),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ class GlassSurface extends StatelessWidget {
|
||||
const GlassSurface({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.radius = const BorderRadius.all(Radius.zero),
|
||||
this.radius = const BorderRadius.all(Radius.circular(14)),
|
||||
this.padding = EdgeInsets.zero,
|
||||
this.tint,
|
||||
this.onDark = false,
|
||||
@@ -73,7 +73,7 @@ Widget glassCircle(
|
||||
children: [
|
||||
GlassSurface(
|
||||
onDark: onDark,
|
||||
radius: BorderRadius.zero,
|
||||
radius: BorderRadius.circular(size),
|
||||
child: SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
|
||||
@@ -32,8 +32,8 @@ Future<Goal?> showGoalPicker(
|
||||
decoration: const BoxDecoration(
|
||||
color: colorSheetBackground,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.zero,
|
||||
topRight: Radius.zero,
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: ListView(
|
||||
@@ -93,7 +93,7 @@ Widget _goalOption(
|
||||
padding: const EdgeInsets.fromLTRB(14, 13, 14, 13),
|
||||
decoration: BoxDecoration(
|
||||
color: colorWhite,
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: selected ? accent.withValues(alpha: 0.5) : colorBorder,
|
||||
width: selected ? 1.5 : 1,
|
||||
|
||||
@@ -6,7 +6,7 @@ import '../utils/CommonUtils.dart';
|
||||
import 'scene/SceneArt.dart';
|
||||
import 'text/Text.dart';
|
||||
|
||||
/// A member's square portrait: a landscape picked from their name with the
|
||||
/// A member's round portrait: a landscape picked from their name with the
|
||||
/// initial on it, so everyone is recognisable without a photo.
|
||||
Widget memberAvatar(String name, double size, {Color? border}) {
|
||||
const List<List<Color>> palettes = <List<Color>>[
|
||||
@@ -21,16 +21,21 @@ Widget memberAvatar(String name, double size, {Color? border}) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration:
|
||||
BoxDecoration(border: Border.all(color: border ?? colorWhite, width: 2)),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
SceneArt(palette: palettes[seed % palettes.length], seed: seed),
|
||||
Center(
|
||||
child: text(initial, size * 0.42, TextType.Medium, color: colorWhite),
|
||||
),
|
||||
],
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: border ?? colorWhite, width: 2),
|
||||
),
|
||||
child: ClipOval(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
SceneArt(palette: palettes[seed % palettes.length], seed: seed),
|
||||
Center(
|
||||
child:
|
||||
text(initial, size * 0.42, TextType.Medium, color: colorWhite),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -61,6 +66,7 @@ Widget avatarStack(List<String> names, double size,
|
||||
height: size,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorNight,
|
||||
border: Border.all(color: border ?? colorWhite, width: 2),
|
||||
),
|
||||
|
||||
@@ -27,6 +27,9 @@ class Sheet extends StatelessWidget {
|
||||
/// Optional trailing control in the chrome.
|
||||
final Widget? action;
|
||||
|
||||
/// Space under the content; tabs leave room for the floating nav.
|
||||
final double bottomSpace;
|
||||
|
||||
/// Stage colour. Leaving it null (with no banner) gives the ambient tone.
|
||||
final Color? chrome;
|
||||
|
||||
@@ -53,6 +56,7 @@ class Sheet extends StatelessWidget {
|
||||
this.scrollable = true,
|
||||
this.scene,
|
||||
this.sceneSeed = 0,
|
||||
this.bottomSpace = 40,
|
||||
});
|
||||
|
||||
bool get _light => chrome == null && banner == null && scene == null;
|
||||
@@ -62,8 +66,7 @@ class Sheet extends StatelessWidget {
|
||||
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 Color soft = light ? colorInkSoft : colorWhite.withValues(alpha: 0.5);
|
||||
|
||||
final Widget header = SafeArea(
|
||||
bottom: false,
|
||||
@@ -80,8 +83,7 @@ class Sheet extends StatelessWidget {
|
||||
? null
|
||||
: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: glassCircle(
|
||||
CupertinoIcons.arrow_left, onBack!,
|
||||
child: glassCircle(CupertinoIcons.arrow_left, onBack!,
|
||||
onDark: !light),
|
||||
),
|
||||
),
|
||||
@@ -136,14 +138,14 @@ class Sheet extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: light ? colorFrost : colorPrimaryLight,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.zero,
|
||||
topRight: Radius.zero,
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: scrollable
|
||||
? SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 28, 20, 40),
|
||||
padding: EdgeInsets.fromLTRB(20, 28, 20, bottomSpace),
|
||||
child: child,
|
||||
)
|
||||
: Padding(
|
||||
|
||||
@@ -13,7 +13,7 @@ Widget roundedCornerButton(
|
||||
Color? foreground,
|
||||
IconData? icon,
|
||||
bool enabled = true,
|
||||
double radius = 99,
|
||||
double radius = 14,
|
||||
double verticalPadding = 16,
|
||||
}) {
|
||||
final Color bg = enabled ? (background ?? colorNight) : colorGrey;
|
||||
@@ -27,7 +27,7 @@ Widget roundedCornerButton(
|
||||
disabledBackgroundColor: colorGrey.withValues(alpha: 0.4),
|
||||
padding: EdgeInsets.symmetric(vertical: verticalPadding),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -55,7 +55,7 @@ Widget outlinedActionButton(
|
||||
Color? foreground,
|
||||
IconData? icon,
|
||||
bool enabled = true,
|
||||
double radius = 99,
|
||||
double radius = 14,
|
||||
}) {
|
||||
final Color fg = enabled ? (foreground ?? colorPrimaryDark) : colorGrey;
|
||||
|
||||
@@ -65,7 +65,7 @@ Widget outlinedActionButton(
|
||||
side: BorderSide(color: fg.withValues(alpha: 0.35), width: 1),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -112,7 +112,7 @@ Widget iconButton(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: background ?? Colors.transparent,
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
border: bordered
|
||||
? Border.all(color: borderColor ?? colorBorder, width: 1)
|
||||
: null,
|
||||
@@ -161,7 +161,7 @@ Widget segmentedSelector<T>({
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? active : colorWhite,
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isSelected ? active : colorBorder,
|
||||
width: 1,
|
||||
|
||||
@@ -17,7 +17,7 @@ Widget countStepper(
|
||||
padding: const EdgeInsets.fromLTRB(14, 10, 8, 10),
|
||||
decoration: BoxDecoration(
|
||||
color: colorWhite,
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: colorBorder, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
@@ -54,7 +54,7 @@ Widget _stepButton(IconData icon, VoidCallback? onTap) {
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: enabled ? colorSecondary : colorInset,
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon,
|
||||
size: 14, color: enabled ? colorPrimaryDark : colorGrey),
|
||||
|
||||
@@ -54,19 +54,19 @@ Widget inputField(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: colorBorder, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: colorPrimaryDark, width: 1.4),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: colorNegative, width: 1),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: colorNegative, width: 1.4),
|
||||
),
|
||||
errorStyle: TextStyle(
|
||||
@@ -163,19 +163,19 @@ Widget excuseField(
|
||||
fillColor: colorWhite,
|
||||
contentPadding: const EdgeInsets.all(14),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: colorBorder, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: colorPrimaryDark, width: 1.4),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: colorNegative, width: 1),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: colorNegative, width: 1.4),
|
||||
),
|
||||
errorStyle: TextStyle(
|
||||
@@ -212,7 +212,7 @@ Widget selectField(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorWhite,
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: colorBorder, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
|
||||
Reference in New Issue
Block a user