import 'package:flutter/material.dart'; import '../../../theme/design_tokens.dart'; enum NooButtonVariant { primary, tonal, secondary, danger, outline, text, textDanger } enum NooButtonSize { cta, card, field, toolbar, compact, xs } class _SizeSpec { final double height; final double px; final double font; final double icon; final double gap; const _SizeSpec(this.height, this.px, this.font, this.icon, this.gap); } const _sizes = { NooButtonSize.cta: _SizeSpec(52, 20, 16, 20, 10), NooButtonSize.card: _SizeSpec(44, 18, 15, 16, 8), NooButtonSize.field: _SizeSpec(40, 16, 14, 16, 6), NooButtonSize.toolbar: _SizeSpec(36, 16, 14, 16, 8), NooButtonSize.compact: _SizeSpec(32, 14, 13, 14, 6), NooButtonSize.xs: _SizeSpec(28, 12, 12, 14, 6), }; /// The pill button for every action in the design system /// (Noo Design System project, `components/core/Button.jsx`). Pick /// [NooButtonVariant] by emphasis and [NooButtonSize] by context - never /// build a bespoke `ElevatedButton`/`FilledButton` for app chrome; extend /// this instead. class NooButton extends StatefulWidget { final NooButtonVariant variant; final NooButtonSize size; final IconData? icon; final Widget? child; final bool fullWidth; final bool disabled; final VoidCallback? onTap; const NooButton({ super.key, this.variant = NooButtonVariant.primary, this.size = NooButtonSize.toolbar, this.icon, this.child, this.fullWidth = false, this.disabled = false, this.onTap, }); @override State createState() => _NooButtonState(); } class _NooButtonState extends State { bool _down = false; (Color, Color, BoxBorder?) _colors(NooColors colors) { switch (widget.variant) { case NooButtonVariant.primary: return (colors.accent, Colors.white, null); case NooButtonVariant.tonal: return (colors.accentSoft, colors.accentText, null); case NooButtonVariant.secondary: return (colors.surface2, colors.fg1, null); case NooButtonVariant.danger: return (colors.dangerSoft, colors.danger, null); case NooButtonVariant.outline: return ( Colors.transparent, colors.fg1, Border.all(color: colors.surface3, width: 1.5), ); case NooButtonVariant.text: return (Colors.transparent, colors.accentText, null); case NooButtonVariant.textDanger: return (Colors.transparent, colors.danger, null); } } bool get _isTextVariant => widget.variant == NooButtonVariant.text || widget.variant == NooButtonVariant.textDanger; @override Widget build(BuildContext context) { final colors = context.nooColors; final s = _sizes[widget.size]!; final (bg, fg, border) = _colors(colors); final hasLeadingIcon = widget.icon != null && widget.child != null && !_isTextVariant; final content = Row( mainAxisSize: MainAxisSize.min, children: [ if (widget.icon != null) Icon(widget.icon, size: s.icon, color: fg), if (widget.icon != null && widget.child != null) SizedBox(width: s.gap), if (widget.child != null) DefaultTextStyle( style: NooText.buttonSm.copyWith(fontSize: s.font, color: fg), child: widget.child!, ), ], ); return Opacity( opacity: widget.disabled ? 0.45 : 1, child: GestureDetector( onTapDown: widget.disabled ? null : (_) => setState(() => _down = true), onTapUp: widget.disabled ? null : (_) => setState(() => _down = false), onTapCancel: widget.disabled ? null : () => setState(() => _down = false), onTap: widget.disabled ? null : widget.onTap, child: AnimatedScale( scale: _down && !widget.disabled ? NooMotion.pressScale : 1, duration: NooMotion.fast, curve: NooMotion.ease, child: Container( height: s.height, width: widget.fullWidth ? double.infinity : null, padding: _isTextVariant ? EdgeInsets.zero : EdgeInsets.only( left: hasLeadingIcon ? s.px - 4 : s.px, right: s.px, ), // Only align when fullWidth actually expands this Container - // otherwise (no width, alignment set) Container wraps the child // in an Align that fills all available *bounded* space instead // of shrink-wrapping to it, which is what stretched this button // edge-to-edge instead of sizing to its content (see NooFab's // doc comment for the same bug). alignment: widget.fullWidth ? Alignment.center : null, decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(NooRadii.pill), border: border, ), child: content, ), ), ), ); } }