billsplit: adopt the shared palette, retire the teal seed

Last of the four drifted apps. The theme was ColorScheme.fromSeed(teal)
plus fifteen ad hoc colour literals that mostly bypassed it -- not one
widget outside main.dart read colorScheme at all, so the seed barely
affected what you saw. It now uses design_system-v0.1.0's
buildLightTheme(), and the literals that have a real theme role read it
off the scheme.

Light, not dark, deliberately: the receipt screens carry black-on-white
helper text, so inverting brightness would make it unreadable. The
Colors.black54 sites are exactly those, now onSurfaceVariant.

The error family maps cleanly -- Colors.red to error, red.shade50 to
errorContainer with onErrorContainer text on it, and the swipe-to-delete
background+icon to error/onError so the fill and the glyph drawn on it
cannot drift apart.

The four category dots (deepOrange/amber/blueGrey/teal) stay literals,
with a comment saying why: they encode which category an item is, not a
theme role, and a single-accent system has no four distinct hues to lend
them. Mapping them onto primary/secondary/tertiary would make the
categories stop being distinguishable, which is their only job. They are
tracked in nielsen-audit.md as needing a categorical ramp. Note this
decouples the default dot from the old seed -- both were Colors.teal, so
they used to match by coincidence rather than by design.

Verified: 60/60 tests, 100% line coverage across 11 lib files, analyze
clean apart from four pre-existing unnecessary_unawaited infos. A
temporary assertion pumped the real app and read primary=ffb8862e and
surface=fff6f4f3 off a live Scaffold, confirming the palette changed
rather than merely compiling. Colour literals: 15 -> 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL
This commit is contained in:
Krzysztof Rudnicki 2026-08-14 23:35:08 +02:00
parent 5c52de0682
commit 9f5400919e
8 changed files with 74 additions and 27 deletions

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:billsplit/state/app_state.dart';
import 'package:billsplit/ui/home_screen.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -27,9 +28,10 @@ class BillSplitApp extends StatelessWidget {
child: MaterialApp(
title: 'BillSplit',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
),
// The shared light theme, replacing a teal-seeded scheme. Light, not
// dark: the receipt screens carry black-on-white helper text, so
// switching brightness here would make it unreadable.
theme: buildLightTheme(),
home: const HomeScreen(),
),
);

View File

@ -1,6 +1,7 @@
import 'package:billsplit/domain/models.dart';
import 'package:billsplit/domain/split_engine.dart';
import 'package:billsplit/state/app_state.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Bottom sheet to assign an item: quick modes (everyone / drinkers /
@ -126,8 +127,10 @@ Future<void> showAssignmentSheet(
'totals until someone is assigned.'
: _eachLabel(resolved.length, item.totalGr),
style: TextStyle(
color: resolved.isEmpty ? Colors.red : Colors.black54,
fontSize: 12,
color: resolved.isEmpty
? Theme.of(c).colorScheme.error
: Theme.of(c).colorScheme.onSurfaceVariant,
fontSize: AppTextSize.caption,
),
),
],

View File

@ -6,6 +6,7 @@ import 'package:billsplit/domain/models.dart';
import 'package:billsplit/state/app_state.dart';
import 'package:billsplit/ui/people_screen.dart';
import 'package:billsplit/ui/receipt_screen.dart';
import 'package:design_system/design_system.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -45,11 +46,17 @@ class HomeScreen extends StatelessWidget {
return Dismissible(
key: ValueKey(r.id),
direction: DismissDirection.endToStart,
// The delete affordance revealed by swiping. The fill
// and the icon on top of it must move together, hence
// error/onError rather than two independent colours.
background: Container(
color: Colors.red.shade300,
color: Theme.of(context).colorScheme.error,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 16),
child: const Icon(Icons.delete, color: Colors.white),
padding: const EdgeInsets.only(right: AppSpacing.md),
child: Icon(
Icons.delete,
color: Theme.of(context).colorScheme.onError,
),
),
confirmDismiss: (_) => _confirmDelete(context, r),
onDismissed: (_) => state.removeReceipt(r),

View File

@ -1,5 +1,6 @@
import 'package:billsplit/domain/models.dart';
import 'package:billsplit/state/app_state.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Add ([item] == null) or edit an item. The user types amounts in złoty
@ -65,10 +66,13 @@ Future<void> showItemEditDialog(
setState(() => category = v ?? Categories.general),
),
const SizedBox(height: 4),
const Text(
Text(
'Alcohol items default to drinkers only; everything else to '
'everyone. Fine-tune by tapping the item afterwards.',
style: TextStyle(fontSize: 11, color: Colors.black54),
style: TextStyle(
fontSize: AppTextSize.caption,
color: Theme.of(c).colorScheme.onSurfaceVariant,
),
),
],
),
@ -80,7 +84,10 @@ Future<void> showItemEditDialog(
state.mutate(() => receipt.items.remove(item));
Navigator.pop(c);
},
child: const Text('Delete', style: TextStyle(color: Colors.red)),
child: Text(
'Delete',
style: TextStyle(color: Theme.of(c).colorScheme.error),
),
),
TextButton(
onPressed: () => Navigator.pop(c),

View File

@ -1,5 +1,6 @@
import 'package:billsplit/domain/models.dart';
import 'package:billsplit/state/app_state.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -25,12 +26,15 @@ class PeopleScreen extends StatelessWidget {
for (final p in state.people) _PersonTile(person: p),
const Divider(height: 32),
const _SectionHeader('Groups'),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Drinkers / non-drinkers are built in (the toggle on each '
'person). Groups below are extra tags, e.g. "meat eaters".',
style: TextStyle(fontSize: 12, color: Colors.black54),
style: TextStyle(
fontSize: AppTextSize.caption,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
for (final g in state.groups) _GroupTile(group: g),

View File

@ -234,7 +234,7 @@ class _ItemsList extends StatelessWidget {
subtitle: Text(
_assignmentLabel(item, people.length, state),
style: TextStyle(
color: unassigned ? Colors.red : null,
color: unassigned ? Theme.of(context).colorScheme.error : null,
fontWeight: unassigned ? FontWeight.bold : null,
),
),
@ -272,6 +272,12 @@ class _CategoryDot extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = switch (category) {
// Categorical encoding, deliberately NOT mapped onto ColorScheme slots.
// These four say "which category", not "which theme role", and a
// single-accent design system has no four distinct hues to lend. Folding
// them into primary/secondary/tertiary would make the categories stop
// being distinguishable, which is the only job they have. Tracked in
// unified-design-system/nielsen-audit.md as needing a categorical ramp.
Categories.alcohol => Colors.deepOrange,
Categories.mixer => Colors.amber,
Categories.deposit => Colors.blueGrey,
@ -300,14 +306,16 @@ class _TotalsView extends StatelessWidget {
children: [
if (split.unassigned.isNotEmpty)
Card(
color: Colors.red.shade50,
color: Theme.of(context).colorScheme.errorContainer,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
'${split.unassigned.length} item(s) have nobody assigned and '
'are excluded from totals:\n'
'${split.unassigned.map((i) => '· ${i.name}').join('\n')}',
style: const TextStyle(color: Colors.red),
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
),
@ -347,7 +355,7 @@ class _TotalsView extends StatelessWidget {
}) {
final style = TextStyle(
fontWeight: bold || warn ? FontWeight.bold : null,
color: warn ? Colors.red : null,
color: warn ? Theme.of(context).colorScheme.error : null,
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),

View File

@ -97,6 +97,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.7"
design_system:
dependency: "direct main"
description:
path: design_system
ref: "design_system-v0.1.0"
resolved-ref: e6c9af81e9ac3000d87d9168d3ef87ba2979258b
url: "https://github.com/kuhyx/utils"
source: git
version: "0.1.0"
equatable:
dependency: transitive
description:
@ -332,10 +341,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.19"
version: "0.12.20"
material_color_utilities:
dependency: transitive
description:
@ -348,10 +357,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.0"
nested:
dependency: transitive
description:
@ -529,10 +538,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.12"
typed_data:
dependency: transitive
description:
@ -545,10 +554,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.4.2"
very_good_analysis:
dependency: "direct dev"
description:
@ -598,5 +607,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.12.0 <4.0.0"
dart: ">=3.12.2 <4.0.0"
flutter: ">=3.44.0"

View File

@ -7,6 +7,13 @@ environment:
sdk: ^3.5.0
dependencies:
# The shared palette and scales.
design_system:
git:
url: https://github.com/kuhyx/utils
ref: design_system-v0.1.0
path: design_system
excel: ^4.0.6
file_selector: ^1.1.0
flutter: