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:
parent
6de039d518
commit
28e8318026
15 changed files with 364 additions and 53 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue