mirror of
https://github.com/we-promise/sure.git
synced 2026-07-13 05:15:19 +00:00
* feat(mobile): add SureChip primitive and migrate the currency filter Adds SureChip, the filter-chip primitive following SureTextField in the mobile design-system sequence: a tokenized selectable pill resolving from the active SureColors palette — a bordered transparent pill when unselected and a filled neutral (buttonPrimary + inverse label) pill when selected — replacing the Material FilterChip primaryContainer tint + blue primary border. Includes selected-state semantics (announced as a button only when interactive), a 44px min tap target, and single-line ellipsised labels. Migrates the currency filter off Material FilterChip as proof (selection logic preserved). The segmented control will follow as a separate PR. Part of #2235. * fix(mobile): normalize stale codes in currency filter All detection Length-based All detection could misfire when selectedCurrencies holds stale codes no longer in availableCurrencies. Intersect with the available set before comparing, and require containsAll before emitting the empty (All) selection. Addresses CodeRabbit review.
103 lines
3.3 KiB
Dart
103 lines
3.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/sure_colors.dart';
|
|
|
|
/// Sure design-system filter chip — a tokenized selectable pill mirroring the web
|
|
/// DS pill: a rounded-full chip that reads as bordered/neutral when unselected
|
|
/// and filled (neutral `buttonPrimary` + inverse label) when selected.
|
|
///
|
|
/// Colors resolve from the active [SureColors] palette, so it's brightness-aware
|
|
/// and stays in lockstep with `sure.tokens.json` (and avoids the Material
|
|
/// `primaryContainer` tint the raw `FilterChip` falls back to).
|
|
///
|
|
/// ```dart
|
|
/// SureChip(
|
|
/// label: 'USD',
|
|
/// selected: isSelected,
|
|
/// onSelected: (next) => toggle(next),
|
|
/// )
|
|
/// ```
|
|
class SureChip extends StatelessWidget {
|
|
const SureChip({
|
|
super.key,
|
|
required this.label,
|
|
this.selected = false,
|
|
this.onSelected,
|
|
this.leading,
|
|
this.enabled = true,
|
|
});
|
|
|
|
final String label;
|
|
final bool selected;
|
|
|
|
/// Called with the next selected value when tapped. When null the chip is
|
|
/// non-interactive (still renders its selected/unselected state).
|
|
final ValueChanged<bool>? onSelected;
|
|
|
|
/// Optional leading widget (e.g. a color dot or icon).
|
|
final Widget? leading;
|
|
|
|
final bool enabled;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final palette = SureColors.of(context).palette;
|
|
final theme = Theme.of(context);
|
|
final interactive = enabled && onSelected != null;
|
|
|
|
// Selected: filled neutral pill with an inverse label. Unselected: a
|
|
// transparent pill with a hairline border. The border lives on the shape so
|
|
// Material paints it and clips the ink to the stadium.
|
|
final shape = StadiumBorder(
|
|
side: selected
|
|
? BorderSide.none
|
|
: BorderSide(color: palette.borderSecondary),
|
|
);
|
|
|
|
final content = ConstrainedBox(
|
|
// Enforce a comfortable minimum tap target regardless of context
|
|
// (Material FilterChip parity); the chip still sizes to content otherwise.
|
|
constraints: const BoxConstraints(minHeight: 44),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (leading != null) ...[leading!, const SizedBox(width: 6)],
|
|
Text(
|
|
label,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
color: selected ? palette.textInverse : palette.textSecondary,
|
|
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
return Semantics(
|
|
// Announce as a button only when it's actually tappable; a display-only
|
|
// chip is just a selected indicator, not a disabled button.
|
|
button: interactive ? true : null,
|
|
enabled: interactive ? true : null,
|
|
selected: selected,
|
|
child: Opacity(
|
|
opacity: enabled ? 1.0 : 0.5,
|
|
child: Material(
|
|
color: selected ? palette.buttonPrimary : const Color(0x00000000),
|
|
shape: shape,
|
|
clipBehavior: Clip.antiAlias,
|
|
child: InkWell(
|
|
onTap: interactive ? () => onSelected!(!selected) : null,
|
|
customBorder: shape,
|
|
child: content,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|