refactor(ui): one canonical segment control (ChainSegments) everywhere
Some checks failed
Security / Security check (push) Failing after 1s

The same single-select choice pattern appeared as four widgets:
audit's hover pills, the store's SegmentedButton, the store filter
dialog's ChoiceChips, and the approvals TabBar (usertest finding
#14 / night-log decision 'pill segment as canon'). The audit
pattern is promoted to a shared ChainSegments widget (optional
icons, hover, selected border, button+selected semantics) and all
four sites use it; approvals switches lists via IndexedStack so
both stay alive and switching does not refetch.

Guard per the no-bugfix-without-a-guard rule: widget tests for
selection + semantics, plus a canon sweep that bans
TabBar/TabBarView/TabController/SegmentedButton/ChoiceChip from
lib/ (comments exempt). Deliberately out of scope: the flow
editor's Graph/Text/Run tabs live in the separate editor package.
Studio 0.75.0; guide images regenerated, dark + light verified.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-19 02:58:54 +02:00
parent bfd58baa75
commit ed680c507a
9 changed files with 311 additions and 147 deletions

View file

@ -365,25 +365,22 @@ class _StorePageState extends State<StorePage> {
Padding(
padding:
const EdgeInsets.only(bottom: ChainSpace.md),
child: SegmentedButton<bool>(
segments: [
ButtonSegment(
value: false,
label: Text(l.storeSegmentModules),
icon: const Icon(Icons.extension_outlined,
size: 16),
child: ChainSegments<bool>(
items: [
ChainSegmentItem(
false,
l.storeSegmentModules,
icon: Icons.extension_outlined,
),
ButtonSegment(
value: true,
label: Text(l.storeSegmentStudio),
icon:
const Icon(Icons.palette_outlined, size: 16),
ChainSegmentItem(
true,
l.storeSegmentStudio,
icon: Icons.palette_outlined,
),
],
selected: {_showStudio},
showSelectedIcon: false,
onSelectionChanged: (s) =>
setState(() => _showStudio = s.first),
value: _showStudio,
onChanged: (v) =>
setState(() => _showStudio = v),
),
),
if (_aiThinking ||
@ -1161,17 +1158,10 @@ class _FilterDialogState extends State<_FilterDialog> {
List<({String value, String label})> items,
String selected,
void Function(String) onSelect,
) => Wrap(
spacing: ChainSpace.xs,
runSpacing: ChainSpace.xs,
children: [
for (final i in items)
_ChoiceChip(
label: i.label,
selected: selected == i.value,
onSelected: () => onSelect(i.value),
),
],
) => ChainSegments<String>(
items: [for (final i in items) ChainSegmentItem(i.value, i.label)],
value: selected,
onChanged: onSelect,
);
return AlertDialog(
title: Text(l.storeFilterLabel),
@ -1569,31 +1559,6 @@ Up to 5 matches, ordered by best fit first. Use only module names that appear in
}
}
class _ChoiceChip extends StatelessWidget {
final String label;
final bool selected;
final VoidCallback onSelected;
const _ChoiceChip({
required this.label,
required this.selected,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: ChainSpace.xs),
child: ChoiceChip(
label: Text(label),
selected: selected,
onSelected: (_) => onSelected(),
showCheckmark: false,
),
);
}
}
/// Responsive grid wraps to as many columns as the viewport
/// allows. Cards are click-to-expand; install is also a top-
/// level action so single-click installs are still possible.