Files
shaynee-flutter/lib/widgets/s_button.dart
mav 6311088256 redesign: duolingo-pattern onboarding, no gradient, pressable button
- Nunito font replacing Plus Jakarta Sans
- SButton: Stack-based 3D pressable (dark shadow layer + top layer translates on press)
- SCard: flat border-based selection, no shadows
- SProgressBar: 4px thin bar, animated fill
- All screens: white bg, no gradients
- Duolingo layout: illustration top zone, content below, sticky button
2026-05-28 16:34:04 +02:00

89 lines
2.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme.dart';
// Duolingo-style pressable button.
// Stack of two containers: dark "shadow" bottom + colored top that translates down on press.
class SButton extends StatefulWidget {
final String label;
final VoidCallback? onTap;
final bool enabled;
final Color? color;
final Color? shadowColor;
final Color? textColor;
const SButton({
super.key,
required this.label,
required this.onTap,
this.enabled = true,
this.color,
this.shadowColor,
this.textColor,
});
@override
State<SButton> createState() => _SButtonState();
}
class _SButtonState extends State<SButton> {
bool _pressed = false;
bool get _active => widget.enabled && widget.onTap != null;
static const _shadowH = 4.0;
static const _btnH = 52.0;
static const _radius = 16.0;
@override
Widget build(BuildContext context) {
final bg = _active ? (widget.color ?? SColors.primary) : const Color(0xFFE5E5E5);
final shadow = _active ? (widget.shadowColor ?? SColors.primaryDark) : const Color(0xFFB8B8B8);
final textCol = _active ? (widget.textColor ?? Colors.white) : SColors.textDisabled;
final offset = (_pressed && _active) ? _shadowH : 0.0;
return GestureDetector(
onTapDown: (_) { if (_active) setState(() => _pressed = true); },
onTapUp: (_) {
if (!_active) return;
setState(() => _pressed = false);
HapticFeedback.mediumImpact();
widget.onTap!();
},
onTapCancel: () { if (_active) setState(() => _pressed = false); },
child: SizedBox(
height: _btnH + _shadowH,
child: Stack(
children: [
// Shadow layer (always at bottom)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: shadow,
borderRadius: BorderRadius.circular(_radius),
),
),
),
// Button layer (translates down on press, "sinking" into shadow)
AnimatedPositioned(
duration: const Duration(milliseconds: 80),
top: offset,
left: 0,
right: 0,
height: _btnH,
child: Container(
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(_radius),
),
alignment: Alignment.center,
child: Text(widget.label, style: SText.button(textCol)),
),
),
],
),
),
);
}
}