From 0e6ef13d00854e62c2dd46d6e6ed3b1f038cf61b Mon Sep 17 00:00:00 2001 From: vjrj Date: Fri, 10 Jul 2026 02:19:07 +0200 Subject: [PATCH] fix(ui): pin edit-sheet Save button as a fixed footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The variety edit sheet grew with v8 fields (organic, needs-reproduction, crop calendar) and its Save button, sitting at the bottom of the SingleChildScrollView, dropped below the fold — the species-linking widget test could not reach it (tap landed off-screen, species never linked). Move the fields into a Flexible scroll area and pin the Cancel/Save row as a fixed footer so Save is always visible and tappable. Full suite green (233 tests). --- .../lib/ui/variety_detail_screen.dart | 283 +++++++++--------- 1 file changed, 148 insertions(+), 135 deletions(-) diff --git a/apps/app_seeds/lib/ui/variety_detail_screen.dart b/apps/app_seeds/lib/ui/variety_detail_screen.dart index 2debd50..b195c83 100644 --- a/apps/app_seeds/lib/ui/variety_detail_screen.dart +++ b/apps/app_seeds/lib/ui/variety_detail_screen.dart @@ -667,145 +667,158 @@ class _EditVarietySheetState extends State<_EditVarietySheet> { top: 16, bottom: MediaQuery.of(context).viewInsets.bottom + 16, ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - t.editVariety.title, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 12), - TextField( - key: const Key('editVariety.name'), - controller: _name, - textCapitalization: TextCapitalization.sentences, - decoration: InputDecoration( - labelText: t.editVariety.name, - border: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), + // Fields scroll; the Cancel/Save row is pinned as a fixed footer so Save + // stays visible and tappable no matter how tall the sheet grows. + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + t.editVariety.title, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 12), + TextField( + key: const Key('editVariety.name'), + controller: _name, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + labelText: t.editVariety.name, + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + const SizedBox(height: 12), + TextField( + key: const Key('editVariety.species'), + controller: _speciesField, + decoration: InputDecoration( + labelText: t.editVariety.species, + hintText: t.editVariety.speciesHint, + prefixIcon: const Icon(Icons.eco_outlined), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + onChanged: _onSpeciesQuery, + ), + for (final match in _suggestions) + ListTile( + dense: true, + key: Key('species.option.${match.id}'), + title: Text(match.displayLabel), + subtitle: match.family == null + ? null + : Text(match.family!), + onTap: () => _pick(match), + ), + const SizedBox(height: 12), + TextField( + controller: _category, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + labelText: t.editVariety.category, + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + const SizedBox(height: 12), + TextField( + controller: _notes, + minLines: 2, + maxLines: 5, + decoration: InputDecoration( + labelText: t.editVariety.notes, + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + const SizedBox(height: 4), + SwitchListTile( + key: const Key('editVariety.organic'), + contentPadding: EdgeInsets.zero, + secondary: const Icon(Icons.eco_outlined, color: seedGreen), + title: Text(t.editVariety.organic), + subtitle: Text(t.editVariety.organicHint), + value: _isOrganic, + onChanged: (v) => setState(() => _isOrganic = v), + ), + SwitchListTile( + key: const Key('editVariety.needsReproduction'), + contentPadding: EdgeInsets.zero, + secondary: const Icon(Icons.autorenew, color: seedGreen), + title: Text(t.needsReproduction.label), + subtitle: Text(t.needsReproduction.hint), + value: _needsReproduction, + onChanged: (v) => setState(() => _needsReproduction = v), + ), + // Crop calendar: one collapsed reveal, not five dropdowns up front. + ExpansionTile( + key: const Key('editVariety.cropCalendar'), + initiallyExpanded: _calendarOpen, + tilePadding: EdgeInsets.zero, + childrenPadding: const EdgeInsets.only(bottom: 8), + leading: const Icon(Icons.calendar_month_outlined), + title: Text(t.cropCalendar.title), + onExpansionChanged: (v) => _calendarOpen = v, + children: [ + _MonthMultiSelect( + label: t.cropCalendar.sow, + mask: _sowMonths, + onChanged: (m) => setState(() => _sowMonths = m), + ), + _MonthMultiSelect( + label: t.cropCalendar.transplant, + mask: _transplantMonths, + onChanged: (m) => setState(() => _transplantMonths = m), + ), + _MonthMultiSelect( + label: t.cropCalendar.flowering, + mask: _floweringMonths, + onChanged: (m) => setState(() => _floweringMonths = m), + ), + _MonthMultiSelect( + label: t.cropCalendar.fruiting, + mask: _fruitingMonths, + onChanged: (m) => setState(() => _fruitingMonths = m), + ), + _MonthMultiSelect( + label: t.cropCalendar.seedHarvest, + mask: _seedHarvestMonths, + onChanged: (m) => + setState(() => _seedHarvestMonths = m), + ), + ], + ), + ], ), ), - const SizedBox(height: 12), - TextField( - key: const Key('editVariety.species'), - controller: _speciesField, - decoration: InputDecoration( - labelText: t.editVariety.species, - hintText: t.editVariety.speciesHint, - prefixIcon: const Icon(Icons.eco_outlined), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), + ), + const SizedBox(height: 16), + Row( + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(t.common.cancel), ), - onChanged: _onSpeciesQuery, - ), - for (final match in _suggestions) - ListTile( - dense: true, - key: Key('species.option.${match.id}'), - title: Text(match.displayLabel), - subtitle: match.family == null ? null : Text(match.family!), - onTap: () => _pick(match), + const Spacer(), + FilledButton( + key: const Key('editVariety.save'), + onPressed: _save, + child: Text(t.common.save), ), - const SizedBox(height: 12), - TextField( - controller: _category, - textCapitalization: TextCapitalization.sentences, - decoration: InputDecoration( - labelText: t.editVariety.category, - border: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - ), - ), - const SizedBox(height: 12), - TextField( - controller: _notes, - minLines: 2, - maxLines: 5, - decoration: InputDecoration( - labelText: t.editVariety.notes, - border: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - ), - ), - const SizedBox(height: 4), - SwitchListTile( - key: const Key('editVariety.organic'), - contentPadding: EdgeInsets.zero, - secondary: const Icon(Icons.eco_outlined, color: seedGreen), - title: Text(t.editVariety.organic), - subtitle: Text(t.editVariety.organicHint), - value: _isOrganic, - onChanged: (v) => setState(() => _isOrganic = v), - ), - SwitchListTile( - key: const Key('editVariety.needsReproduction'), - contentPadding: EdgeInsets.zero, - secondary: const Icon(Icons.autorenew, color: seedGreen), - title: Text(t.needsReproduction.label), - subtitle: Text(t.needsReproduction.hint), - value: _needsReproduction, - onChanged: (v) => setState(() => _needsReproduction = v), - ), - // Crop calendar: one collapsed reveal, not five dropdowns up front. - ExpansionTile( - key: const Key('editVariety.cropCalendar'), - initiallyExpanded: _calendarOpen, - tilePadding: EdgeInsets.zero, - childrenPadding: const EdgeInsets.only(bottom: 8), - leading: const Icon(Icons.calendar_month_outlined), - title: Text(t.cropCalendar.title), - onExpansionChanged: (v) => _calendarOpen = v, - children: [ - _MonthMultiSelect( - label: t.cropCalendar.sow, - mask: _sowMonths, - onChanged: (m) => setState(() => _sowMonths = m), - ), - _MonthMultiSelect( - label: t.cropCalendar.transplant, - mask: _transplantMonths, - onChanged: (m) => setState(() => _transplantMonths = m), - ), - _MonthMultiSelect( - label: t.cropCalendar.flowering, - mask: _floweringMonths, - onChanged: (m) => setState(() => _floweringMonths = m), - ), - _MonthMultiSelect( - label: t.cropCalendar.fruiting, - mask: _fruitingMonths, - onChanged: (m) => setState(() => _fruitingMonths = m), - ), - _MonthMultiSelect( - label: t.cropCalendar.seedHarvest, - mask: _seedHarvestMonths, - onChanged: (m) => setState(() => _seedHarvestMonths = m), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(t.common.cancel), - ), - const Spacer(), - FilledButton( - key: const Key('editVariety.save'), - onPressed: _save, - child: Text(t.common.save), - ), - ], - ), - ], - ), + ], + ), + ], ), ); }