- 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
89 lines
2.6 KiB
Dart
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)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|