polish(ux): drop the checkmark on selected filter chips

The tick overlapped the leading icon on selected chips, reading as a
confusing blob. The solid fill already signals 'on', so hide the
checkmark. Also make the selected label reliably white via an explicit
per-chip style: the global ChipTheme's secondaryLabelStyle was not
applied to FilterChips, leaving dark text on green. Plain attribute/
market chips now share a PlainFilterChip widget; swatch and month chips
just drop their tick.
This commit is contained in:
vjrj 2026-07-11 08:05:56 +02:00
parent 5ecdd1222a
commit 9f01c3893a
5 changed files with 68 additions and 59 deletions

View file

@ -157,6 +157,7 @@ class _MonthStripState extends State<_MonthStrip> {
final selected = m == widget.month; final selected = m == widget.month;
return ChoiceChip( return ChoiceChip(
key: Key('calendar.month.$m'), key: Key('calendar.month.$m'),
showCheckmark: false,
labelPadding: const EdgeInsets.symmetric(horizontal: 6), labelPadding: const EdgeInsets.symmetric(horizontal: 6),
label: Text( label: Text(
_capitalise(fmt.format(DateTime(2000, m))), _capitalise(fmt.format(DateTime(2000, m))),

View file

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'theme.dart';
/// A filter chip for the horizontal filter rows: light with dark ink when idle,
/// a solid green fill with white content when selected no checkmark, since the
/// fill already reads as "on" and a tick over the leading icon just muddles it.
/// White on [seedGreen] meets WCAG AA (see test/ui/theme_contrast_test.dart).
class PlainFilterChip extends StatelessWidget {
const PlainFilterChip({
required this.label,
required this.selected,
required this.onSelected,
this.icon,
super.key,
});
final String label;
final bool selected;
final ValueChanged<bool> onSelected;
/// Optional leading icon (attribute chips carry one; type/category chips do
/// not).
final IconData? icon;
@override
Widget build(BuildContext context) {
final fg = selected ? Colors.white : seedOnSurface;
return FilterChip(
avatar: icon == null
? null
: Icon(icon, size: 18, color: selected ? Colors.white : seedGreen),
label: Text(label),
selected: selected,
onSelected: onSelected,
showCheckmark: false,
selectedColor: seedGreen,
labelStyle: TextStyle(
color: fg,
fontSize: 14,
fontWeight: FontWeight.w500,
),
);
}
}

View file

@ -12,6 +12,7 @@ import '../state/inventory_cubit.dart';
import 'category_palette.dart'; import 'category_palette.dart';
import 'draft_triage.dart'; import 'draft_triage.dart';
import 'edge_fade.dart'; import 'edge_fade.dart';
import 'filter_chips.dart';
import 'label_print_sheet.dart'; import 'label_print_sheet.dart';
import 'quantity_kind_l10n.dart'; import 'quantity_kind_l10n.dart';
import 'quantity_picker.dart'; import 'quantity_picker.dart';
@ -303,50 +304,34 @@ class _FilterBar extends StatelessWidget {
// stable earthy tonality). Grouping + colour makes a long row scannable. // stable earthy tonality). Grouping + colour makes a long row scannable.
final attrChips = <Widget>[ final attrChips = <Widget>[
if (hasSowCalendar) if (hasSowCalendar)
FilterChip( PlainFilterChip(
key: const Key('inventory.filter.sowThisMonth'), key: const Key('inventory.filter.sowThisMonth'),
avatar: Icon( icon: Icons.calendar_month,
Icons.calendar_month, label: t.calendar.filterChip,
size: 18,
color: state.sowThisMonthOnly ? Colors.white : seedGreen,
),
label: Text(t.calendar.filterChip),
selected: state.sowThisMonthOnly, selected: state.sowThisMonthOnly,
onSelected: (_) => cubit.toggleSowThisMonth(), onSelected: (_) => cubit.toggleSowThisMonth(),
), ),
if (hasShared) if (hasShared)
FilterChip( PlainFilterChip(
key: const Key('inventory.filter.sharing'), key: const Key('inventory.filter.sharing'),
avatar: Icon( icon: Icons.volunteer_activism_outlined,
Icons.volunteer_activism_outlined, label: t.share.filterChip,
size: 18,
color: state.sharingOnly ? Colors.white : seedGreen,
),
label: Text(t.share.filterChip),
selected: state.sharingOnly, selected: state.sharingOnly,
onSelected: (_) => cubit.toggleSharingOnly(), onSelected: (_) => cubit.toggleSharingOnly(),
), ),
if (hasOrganic) if (hasOrganic)
FilterChip( PlainFilterChip(
key: const Key('inventory.filter.organic'), key: const Key('inventory.filter.organic'),
avatar: Icon( icon: Icons.eco,
Icons.eco, label: t.editVariety.organic,
size: 18,
color: state.organicOnly ? Colors.white : seedGreen,
),
label: Text(t.editVariety.organic),
selected: state.organicOnly, selected: state.organicOnly,
onSelected: (_) => cubit.toggleOrganicOnly(), onSelected: (_) => cubit.toggleOrganicOnly(),
), ),
if (hasNeedsReproduction) if (hasNeedsReproduction)
FilterChip( PlainFilterChip(
key: const Key('inventory.filter.needsReproduction'), key: const Key('inventory.filter.needsReproduction'),
avatar: Icon( icon: Icons.autorenew,
Icons.autorenew, label: t.inventory.needsReproductionFilter,
size: 18,
color: state.needsReproductionOnly ? Colors.white : seedGreen,
),
label: Text(t.inventory.needsReproductionFilter),
selected: state.needsReproductionOnly, selected: state.needsReproductionOnly,
onSelected: (_) => cubit.toggleNeedsReproductionOnly(), onSelected: (_) => cubit.toggleNeedsReproductionOnly(),
), ),
@ -456,9 +441,9 @@ class _SwatchFilterChip extends StatelessWidget {
avatar: icon == null ? null : Icon(icon, size: 18, color: ink), avatar: icon == null ? null : Icon(icon, size: 18, color: ink),
selected: selected, selected: selected,
onSelected: (_) => onSelected(), onSelected: (_) => onSelected(),
showCheckmark: false,
backgroundColor: swatch.fill, backgroundColor: swatch.fill,
selectedColor: swatch.ink, selectedColor: swatch.ink,
checkmarkColor: Colors.white,
side: BorderSide( side: BorderSide(
color: swatch.ink.withValues(alpha: selected ? 0 : 0.35), color: swatch.ink.withValues(alpha: selected ? 0 : 0.35),
), ),

View file

@ -13,6 +13,7 @@ import '../services/social_service.dart';
import '../services/social_settings.dart'; import '../services/social_settings.dart';
import '../state/offers_cubit.dart'; import '../state/offers_cubit.dart';
import 'edge_fade.dart'; import 'edge_fade.dart';
import 'filter_chips.dart';
import 'market_widgets.dart'; import 'market_widgets.dart';
import 'theme.dart'; import 'theme.dart';
@ -488,28 +489,24 @@ class _MarketFilterBar extends StatelessWidget {
final chips = <Widget>[ final chips = <Widget>[
if (hasOrganic) if (hasOrganic)
FilterChip( PlainFilterChip(
key: const Key('market.filter.organic'), key: const Key('market.filter.organic'),
avatar: Icon( icon: Icons.eco,
Icons.eco, label: t.editVariety.organic,
size: 18,
color: state.organicOnly ? Colors.white : seedGreen,
),
label: Text(t.editVariety.organic),
selected: state.organicOnly, selected: state.organicOnly,
onSelected: (_) => cubit.toggleOrganicOnly(), onSelected: (_) => cubit.toggleOrganicOnly(),
), ),
for (final type in types) for (final type in types)
FilterChip( PlainFilterChip(
key: Key('market.filter.type.${type.name}'), key: Key('market.filter.type.${type.name}'),
label: Text(offerTypeLabel(t, type)), label: offerTypeLabel(t, type),
selected: state.typeFilter.contains(type), selected: state.typeFilter.contains(type),
onSelected: (_) => cubit.toggleType(type), onSelected: (_) => cubit.toggleType(type),
), ),
for (final category in categories) for (final category in categories)
FilterChip( PlainFilterChip(
key: Key('market.filter.category.$category'), key: Key('market.filter.category.$category'),
label: Text(category), label: category,
selected: state.categoryFilter.contains(category), selected: state.categoryFilter.contains(category),
onSelected: (_) => cubit.toggleCategory(category), onSelected: (_) => cubit.toggleCategory(category),
), ),

View file

@ -105,24 +105,5 @@ ThemeData buildTaneTheme() {
thickness: 1, thickness: 1,
color: seedDivider, color: seedDivider,
), ),
// Filter/choice chips: idle chips stay light with dark ink, but a *selected*
// chip fills solid green with white ink so "on" reads unmistakably against
// the pale canvas (the soft tonal default sat too close to it). White on
// seedGreen meets WCAG AA see test/ui/theme_contrast_test.dart. Chips that
// carry their own hue (family/form swatches) override this per-swatch.
chipTheme: const ChipThemeData(
selectedColor: seedGreen,
checkmarkColor: Colors.white,
labelStyle: TextStyle(
color: seedOnSurface,
fontSize: 14,
fontWeight: FontWeight.w500,
),
secondaryLabelStyle: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
); );
} }