mirror of
https://github.com/we-promise/sure.git
synced 2026-08-05 08:32:15 +00:00
* feat(mobile): add SureSpacing + SureTypography scale tokens Introduce hand-authored spacing and type-scale constants mirroring the Tailwind defaults the web design system relies on, so widgets reference a named step instead of a raw numeric EdgeInsets/SizedBox/fontSize. - SureSpacing: xs..huge mapping to Tailwind space-1..space-8 (4..32px). - SureTypography: xs..xxl mapping to Tailwind text-xs..text-2xl font sizes. Both are hand-written rather than generated from sure.tokens.json because spacing and the type ramp come from Tailwind's built-in scale, not the canonical token file (consistent with the tracker's guidance). Adopt them in the existing primitives (card padding, button metrics + gap, chip/segmented/list-group gaps and padding, text-field padding + label gap). All migrations are value-preserving — each token equals the literal it replaces — so there is no layout change; off-scale one-offs (control heights, hairlines, deliberate 14px field padding) stay literal. flutter analyze: no new issues; full suite (166) green. * design-system(mobile): polish Dashboard with Sure tokens Align the dashboard with the Sure design system (no behavior changes). NetWorthCard: the hero card adopts the canonical Sure card chrome — container fill, hairline borderSecondary, radiusLg, and the subtle DS shadow (mirroring SureCard/AccountCard) instead of Material surfaceContainerHighest/outline with an ad-hoc radius and no elevation. Dividers, the Net Worth label/value, the Outdated badge, asset/liability totals, and the currency-breakdown sheet all resolve from the active SureColors palette (brightness-aware). dashboard_screen.dart: empty/error states use SureButton + palette colors; the account-type group header badge uses surfaceInset/textSecondary + SureTypography; the sync success banner and sync/refresh snackbars use palette.success/ palette.destructive; spacing moves onto the SureSpacing scale. Adds net_worth_card_test.dart asserting the hero card chrome resolves Sure tokens in light and dark. Builds on the SureSpacing/SureTypography scale tokens (#2438). * fix(mobile): readable foreground on tokenized dashboard snackbars + keyed chrome test Address review feedback on #2457: - Snackbar contrast: the success/error snackbars switched their background to palette.success/palette.destructive but kept a white icon + default white text. In dark theme palette.success is a bright green (#32D583), so white was low-contrast. Set the icon and text foreground to palette.textInverse, which flips with the theme (#FFFFFF light / #171717 dark) and stays readable on both semantic fills. - Test robustness: key the NetWorthCard chrome Container ('netWorthCardChrome') and look it up with find.byKey instead of the fragile first-descendant Container match. * fix(mobile): SureButton owns leading-icon foreground via IconTheme Address review feedback (jjmata): call sites shouldn't hardcode the button's foreground on leading icons. Wrap SureButton's content in an IconTheme set to the variant foreground, so leading icons (e.g. SureIcon) inherit it automatically — mirroring how Material's ElevatedButton.icon propagates icon color. Icons that pass an explicit color still win. Drop the now-redundant `color: palette.textInverse` from the dashboard empty/error-state button icons; they follow the button variant automatically. Add a SureButton test asserting a leading icon inherits the variant foreground (textInverse for primary, textPrimary for outline) via the ambient IconTheme. * Fix net worth card mask merge regression * Provide privacy state in net worth card tests --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: sure-admin <sure-admin@splashblot.com>
104 lines
3.4 KiB
Dart
104 lines
3.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/sure_colors.dart';
|
|
import '../theme/sure_spacing.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: SureSpacing.lg),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (leading != null) ...[leading!, const SizedBox(width: SureSpacing.sm)],
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|