polish(ux): AA-contrast chip inks, 14-tone palette, calendar month auto-scroll
Blind-build polish pass over the recent sales/chips/calendar work: - Chip label contrast: 7 palette inks (terracotta, ochre, rose, seedling, plant, bulb, cutting) sat below WCAG AA on their own fill; darkened (hue preserved) to ≥4.8:1. New category_palette_contrast_test locks it. - Palette expanded 8 → 14 earthy tones (hues spread around the wheel) so family colours rarely collide for a typical 6–10 family inventory, keeping the stable-by-name, cross-screen-consistent scheme. Every new ink meets AA on its fill. - Calendar month strip now actually scrolls the selected month into view (the comment promised it but it was never wired) — late-year months are no longer left off-screen when the screen opens. - Minor: dead code removed from the month chip. dart analyze clean; palette/contrast/calendar/quantity/overflow/inventory suites green.
This commit is contained in:
parent
aec14f6518
commit
dcda349878
3 changed files with 97 additions and 16 deletions
|
|
@ -102,13 +102,43 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
|||
}
|
||||
|
||||
/// A horizontally scrolling strip of the twelve months; the picked one is
|
||||
/// filled. Scrolls the picked month into view on first build.
|
||||
class _MonthStrip extends StatelessWidget {
|
||||
/// filled. Scrolls the picked month into view (so late-year months aren't left
|
||||
/// off-screen when the screen opens in, say, November).
|
||||
class _MonthStrip extends StatefulWidget {
|
||||
const _MonthStrip({required this.month, required this.onSelect});
|
||||
|
||||
final int month;
|
||||
final ValueChanged<int> onSelect;
|
||||
|
||||
@override
|
||||
State<_MonthStrip> createState() => _MonthStripState();
|
||||
}
|
||||
|
||||
class _MonthStripState extends State<_MonthStrip> {
|
||||
final _selectedKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_revealSelected();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_MonthStrip old) {
|
||||
super.didUpdateWidget(old);
|
||||
if (old.month != widget.month) _revealSelected();
|
||||
}
|
||||
|
||||
void _revealSelected() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final ctx = _selectedKey.currentContext;
|
||||
if (ctx != null) {
|
||||
Scrollable.ensureVisible(ctx,
|
||||
alignment: 0.5, duration: const Duration(milliseconds: 250));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locale = materialLocaleFor(Localizations.localeOf(context));
|
||||
|
|
@ -122,12 +152,16 @@ class _MonthStrip extends StatelessWidget {
|
|||
separatorBuilder: (_, _) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, i) {
|
||||
final m = i + 1;
|
||||
final selected = m == month;
|
||||
final selected = m == widget.month;
|
||||
return ChoiceChip(
|
||||
key: Key('calendar.month.$m'),
|
||||
label: Text(_capitalise(fmt.format(DateTime(2000, m)))),
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
label: Text(
|
||||
_capitalise(fmt.format(DateTime(2000, m))),
|
||||
key: selected ? _selectedKey : null,
|
||||
),
|
||||
selected: selected,
|
||||
onSelected: (_) => onSelect(m),
|
||||
onSelected: (_) => widget.onSelect(m),
|
||||
selectedColor: seedPrimaryContainer,
|
||||
labelStyle: TextStyle(
|
||||
color: selected ? seedOnPrimaryContainer : seedMuted,
|
||||
|
|
|
|||
|
|
@ -11,19 +11,28 @@ class Swatch {
|
|||
final Color ink;
|
||||
}
|
||||
|
||||
/// A small, muted, earthy palette — one low-saturation key, different hues — so
|
||||
/// botanical families (categories) are distinguishable at a glance without
|
||||
/// shouting over the green brand. Kept calm on purpose (soft fills, legible
|
||||
/// ink). Order is stable; new entries append only.
|
||||
/// A muted, earthy palette — one low-saturation key, hues spread around the
|
||||
/// wheel — so botanical families (categories) are distinguishable at a glance
|
||||
/// without shouting over the green brand. Kept calm on purpose (soft fills,
|
||||
/// legible ink). 14 tones keep collisions rare for a typical 6–10 family
|
||||
/// inventory. Order is stable; new entries append only.
|
||||
// Inks are tuned so the label meets WCAG AA (≥4.5:1) on its own fill — see
|
||||
// test/ui/category_palette_contrast_test.dart.
|
||||
const _familyPalette = <Swatch>[
|
||||
Swatch(Color(0xFFE8EEDA), Color(0xFF556B2F)), // olive
|
||||
Swatch(Color(0xFFF3E2D8), Color(0xFF9C5B3B)), // terracotta
|
||||
Swatch(Color(0xFFF3EBD3), Color(0xFF8A6D1E)), // ochre
|
||||
Swatch(Color(0xFFF3E2D8), Color(0xFF8E5336)), // terracotta
|
||||
Swatch(Color(0xFFF3EBD3), Color(0xFF7C621B)), // ochre
|
||||
Swatch(Color(0xFFDDEAE6), Color(0xFF3E7168)), // sage teal
|
||||
Swatch(Color(0xFFF0E1E4), Color(0xFF9B5566)), // dusty rose
|
||||
Swatch(Color(0xFFF0E1E4), Color(0xFF8F4E5E)), // dusty rose
|
||||
Swatch(Color(0xFFE0E6EE), Color(0xFF4A6489)), // slate blue
|
||||
Swatch(Color(0xFFE9E1EE), Color(0xFF6E5090)), // plum
|
||||
Swatch(Color(0xFFEBE3D8), Color(0xFF7A5A3A)), // warm brown
|
||||
Swatch(Color(0xFFDCEAD6), Color(0xFF3E6B3A)), // forest green
|
||||
Swatch(Color(0xFFD6E7EA), Color(0xFF2C6A73)), // petrol
|
||||
Swatch(Color(0xFFF3DEDB), Color(0xFFA14234)), // brick red
|
||||
Swatch(Color(0xFFEFDCEA), Color(0xFF864C7C)), // mauve
|
||||
Swatch(Color(0xFFE1E1F1), Color(0xFF4E5199)), // indigo
|
||||
Swatch(Color(0xFFDFE4E9), Color(0xFF4C5E70)), // steel
|
||||
];
|
||||
|
||||
/// A stable (launch-to-launch, platform-independent) hash of a category name.
|
||||
|
|
@ -48,9 +57,9 @@ Swatch categorySwatch(String category) =>
|
|||
/// tones for woody/underground ones.
|
||||
Swatch lotTypeSwatch(LotType type) => switch (type) {
|
||||
LotType.seed => const Swatch(seedPrimaryContainer, seedOnPrimaryContainer),
|
||||
LotType.seedling => const Swatch(Color(0xFFDDEBCF), Color(0xFF4E7A2E)),
|
||||
LotType.plant => const Swatch(Color(0xFFD9EAD9), Color(0xFF2F7D34)),
|
||||
LotType.seedling => const Swatch(Color(0xFFDDEBCF), Color(0xFF456D29)),
|
||||
LotType.plant => const Swatch(Color(0xFFD9EAD9), Color(0xFF2A702F)),
|
||||
LotType.tree => const Swatch(Color(0xFFE3D9C8), Color(0xFF6B4F2A)),
|
||||
LotType.bulb => const Swatch(Color(0xFFF3E9CF), Color(0xFF8A6D1E)),
|
||||
LotType.cutting => const Swatch(Color(0xFFE6EAD3), Color(0xFF63702F)),
|
||||
LotType.bulb => const Swatch(Color(0xFFF3E9CF), Color(0xFF7B611B)),
|
||||
LotType.cutting => const Swatch(Color(0xFFE6EAD3), Color(0xFF5D692C)),
|
||||
};
|
||||
|
|
|
|||
38
apps/app_seeds/test/ui/category_palette_contrast_test.dart
Normal file
38
apps/app_seeds/test/ui/category_palette_contrast_test.dart
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/db/enums.dart';
|
||||
import 'package:tane/ui/category_palette.dart';
|
||||
|
||||
/// Chip labels are drawn in [Swatch.ink] over [Swatch.fill]; that pairing must
|
||||
/// clear WCAG AA for normal text (≥4.5:1), like the rest of the palette
|
||||
/// (see theme_contrast_test.dart). Locks the tuned inks against regressions.
|
||||
void main() {
|
||||
double lin(double c) =>
|
||||
c <= 0.03928 ? c / 12.92 : math.pow((c + 0.055) / 1.055, 2.4).toDouble();
|
||||
double luminance(Color c) =>
|
||||
0.2126 * lin(c.r) + 0.7152 * lin(c.g) + 0.0722 * lin(c.b);
|
||||
double contrast(Color a, Color b) {
|
||||
final la = luminance(a), lb = luminance(b);
|
||||
final hi = math.max(la, lb), lo = math.min(la, lb);
|
||||
return (hi + 0.05) / (lo + 0.05);
|
||||
}
|
||||
|
||||
test('every category swatch label meets AA on its fill', () {
|
||||
// Cover the whole palette by probing many distinct category names.
|
||||
for (var i = 0; i < 60; i++) {
|
||||
final s = categorySwatch('family-$i');
|
||||
expect(contrast(s.ink, s.fill), greaterThanOrEqualTo(4.5),
|
||||
reason: 'category-$i ink on fill');
|
||||
}
|
||||
});
|
||||
|
||||
test('every lot-form swatch label meets AA on its fill', () {
|
||||
for (final type in LotType.values) {
|
||||
final s = lotTypeSwatch(type);
|
||||
expect(contrast(s.ink, s.fill), greaterThanOrEqualTo(4.5),
|
||||
reason: '$type ink on fill');
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue