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>
137 lines
3.9 KiB
Dart
137 lines
3.9 KiB
Dart
// ChainSegments — THE single-select segment control (usertest
|
|
// finding #14 / night-log decision "pill segment as canon"):
|
|
// the same choice pattern used to appear as four different
|
|
// widgets (audit's hover pills, store's Material SegmentedButton,
|
|
// store-filter ChoiceChips, approvals' TabBar). One look now:
|
|
// text pills, primary-tinted when selected, quiet hover — the
|
|
// audit page's pattern promoted to a shared widget.
|
|
//
|
|
// Single-select only by design. Multi-select filters are a
|
|
// different pattern (checkbox list), not a segment control.
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/tokens.dart';
|
|
|
|
class ChainSegmentItem<T> {
|
|
final T value;
|
|
final String label;
|
|
final IconData? icon;
|
|
|
|
const ChainSegmentItem(this.value, this.label, {this.icon});
|
|
}
|
|
|
|
class ChainSegments<T> extends StatelessWidget {
|
|
final List<ChainSegmentItem<T>> items;
|
|
final T value;
|
|
final ValueChanged<T> onChanged;
|
|
|
|
const ChainSegments({
|
|
super.key,
|
|
required this.items,
|
|
required this.value,
|
|
required this.onChanged,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Wrap(
|
|
spacing: ChainSpace.xs,
|
|
runSpacing: ChainSpace.xs,
|
|
children: [
|
|
for (final item in items)
|
|
_SegmentPill(
|
|
label: item.label,
|
|
icon: item.icon,
|
|
selected: item.value == value,
|
|
onTap: () {
|
|
if (item.value != value) onChanged(item.value);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SegmentPill extends StatefulWidget {
|
|
final String label;
|
|
final IconData? icon;
|
|
final bool selected;
|
|
final VoidCallback onTap;
|
|
|
|
const _SegmentPill({
|
|
required this.label,
|
|
required this.icon,
|
|
required this.selected,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
State<_SegmentPill> createState() => _SegmentPillState();
|
|
}
|
|
|
|
class _SegmentPillState extends State<_SegmentPill> {
|
|
bool _hovered = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final fg = widget.selected
|
|
? theme.colorScheme.primary
|
|
: _hovered
|
|
? theme.colorScheme.onSurface
|
|
: theme.colorScheme.onSurfaceVariant;
|
|
final bg = widget.selected
|
|
? theme.colorScheme.primary.withValues(alpha: 0.12)
|
|
: _hovered
|
|
? theme.colorScheme.surfaceContainerHigh
|
|
: Colors.transparent;
|
|
return Semantics(
|
|
button: true,
|
|
selected: widget.selected,
|
|
label: widget.label,
|
|
child: MouseRegion(
|
|
onEnter: (_) => setState(() => _hovered = true),
|
|
onExit: (_) => setState(() => _hovered = false),
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
onTap: widget.onTap,
|
|
child: AnimatedContainer(
|
|
duration: ChainMotion.fast,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: ChainSpace.md,
|
|
vertical: 6,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: bg,
|
|
borderRadius: BorderRadius.circular(ChainRadius.sm),
|
|
border: Border.all(
|
|
color: widget.selected
|
|
? theme.colorScheme.primary.withValues(alpha: 0.3)
|
|
: Colors.transparent,
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (widget.icon != null) ...[
|
|
Icon(widget.icon, size: 14, color: fg),
|
|
const SizedBox(width: ChainSpace.xs),
|
|
],
|
|
Text(
|
|
widget.label,
|
|
style: theme.textTheme.labelMedium?.copyWith(
|
|
fontWeight: widget.selected
|
|
? FontWeight.w600
|
|
: FontWeight.w400,
|
|
color: fg,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|