feat(ux): usable amounts, colour-coded category/form chips, currency quick-picks

Four usability passes across the everyday flows:

- Quantity picker: a countable unit (cob/pod/ear…) now defaults to 1 and
  the stepper clamps at a minimum of 1 — you can never store 'a cob' with
  a blank/zero figure. Vibe units (a few) still carry no number.
- Category & form chips are colour-coded (new category_palette.dart): a
  small muted earthy palette maps each botanical family to a STABLE
  tonality (fill + readable ink), and each lot form gets its own tone.
  Applied to the inventory filter bar (now grouped attributes | forms |
  families, separated by a hairline), the list's category headers (a
  matching colour dot), and the lot-form selector.
- Sale currency: quick-pick chips (€ · Ğ1 · hours) fill the field in one
  tap — Ğ1 offered quietly among familiar options, never imposed — while
  free text still takes anything else. New i18n key sale.hours (en/es/pt/ast).

Tests: quantity_picker_test (default-1, clamp, vibe, clear-still-1),
category_palette_test (stable/distinct swatches); overflow guard + the
inventory widget tests stay green.
This commit is contained in:
vjrj 2026-07-11 06:06:04 +02:00
parent 6de039d518
commit 28e8318026
15 changed files with 364 additions and 53 deletions

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import '../db/enums.dart';
import '../i18n/strings.g.dart';
import 'category_palette.dart';
import 'quantity_kind_l10n.dart';
import 'seed_glyph.dart';
import 'theme.dart';
@ -76,18 +77,28 @@ class LotTypeSelector extends StatelessWidget {
runSpacing: 8,
children: [
for (final type in LotType.values)
ChoiceChip(
key: Key('lotType.${type.name}'),
selected: value == type,
onSelected: (_) => onChanged(type),
avatar: type == LotType.seed
? const SeedGlyph(SeedGlyphs.jars, size: 18)
: Icon(iconForLotType(type), size: 18),
label: Text(lotTypeLabel(t, type)),
),
_buildTypeChip(t, type),
],
);
}
Widget _buildTypeChip(Translations t, LotType type) {
final swatch = lotTypeSwatch(type);
final selected = value == type;
return ChoiceChip(
key: Key('lotType.${type.name}'),
selected: selected,
onSelected: (_) => onChanged(type),
avatar: type == LotType.seed
? const SeedGlyph(SeedGlyphs.jars, size: 18)
: Icon(iconForLotType(type), size: 18, color: swatch.ink),
label: Text(lotTypeLabel(t, type)),
backgroundColor: swatch.fill,
selectedColor: swatch.ink.withValues(alpha: 0.24),
side: BorderSide(color: swatch.ink.withValues(alpha: 0.35)),
labelStyle: TextStyle(color: swatch.ink, fontWeight: FontWeight.w500),
);
}
}
/// Optional packaging selector for living lots. Tapping the selected chip again
@ -189,19 +200,29 @@ class _QuantityPickerState extends State<QuantityPicker> {
widget.onChanged(null);
return;
}
final n = num.tryParse(_countController.text.trim().replaceAll(',', '.'));
widget.onChanged(Quantity(kind: kind, count: kind.countable ? n : null));
// A countable unit always carries a number 1 "3 cobs", never an empty
// "cob". Blank/zero/garbage while typing falls back to 1, so you can never
// save a countable amount with no figure.
final parsed = num.tryParse(_countController.text.trim().replaceAll(',', '.'));
final count = kind.countable ? ((parsed == null || parsed < 1) ? 1 : parsed) : null;
widget.onChanged(Quantity(kind: kind, count: count));
}
void _select(QuantityKind kind) {
setState(() => _kind = kind);
setState(() {
_kind = kind;
// Selecting a countable unit with no number yet defaults it to 1.
if (kind.countable && (num.tryParse(_countController.text.trim()) == null)) {
_countController.text = '1';
}
});
_emit();
}
void _bump(int delta) {
final current = int.tryParse(_countController.text.trim()) ?? 0;
final next = (current + delta).clamp(0, 9999);
_countController.text = next == 0 ? '' : '$next';
final current = int.tryParse(_countController.text.trim()) ?? 1;
final next = (current + delta).clamp(1, 9999);
_countController.text = '$next';
_emit();
}
@ -219,6 +240,7 @@ class _QuantityPickerState extends State<QuantityPicker> {
children: [
for (final kind in kinds)
_ScaleItem(
key: Key('quantity.kind.${kind.name}'),
kind: kind,
label: unitLabel(t, kind),
selected: _kind == kind,
@ -273,6 +295,7 @@ class _ScaleItem extends StatelessWidget {
required this.label,
required this.selected,
required this.onTap,
super.key,
});
final QuantityKind kind;