feat(proj): init

This commit is contained in:
vl.arkhangelskii
2026-09-21 04:06:43 +03:00
commit c956b94983
1076 changed files with 50876 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
import 'package:flutter/cupertino.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:please_pay_me/theme/tokens.dart';
/// Circular avatar — image, initials, or the iOS person placeholder.
class AppAvatar extends StatelessWidget {
const AppAvatar({
super.key,
this.imageUrl,
this.initials,
this.radius = 22,
this.backgroundColor = AppColors.systemGray5,
});
final String? imageUrl;
final String? initials;
final double radius;
final Color backgroundColor;
@override
Widget build(BuildContext context) {
final size = radius * 2;
final hasImage = imageUrl != null && imageUrl!.isNotEmpty;
return ClipOval(
child: Container(
width: size,
height: size,
color: AppColors.of(context, backgroundColor),
alignment: Alignment.center,
child: hasImage
? Image.network(imageUrl!, width: size, height: size, fit: BoxFit.cover)
: initials != null && initials!.isNotEmpty
? Text(
initials!.toUpperCase(),
style: GoogleFonts.inter(
textStyle: AppTypography.headline.copyWith(fontSize: radius * 0.8),
color: AppColors.of(context, AppColors.secondaryLabel),
),
)
: Icon(
CupertinoIcons.person_fill,
size: radius,
color: AppColors.of(context, AppColors.systemGray),
),
),
);
}
}
+56
View File
@@ -0,0 +1,56 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/tokens.dart';
import 'package:please_pay_me/ui/atoms/app_text.dart';
/// Inset-grouped card for free-form content (metrics, summaries).
class AppCard extends StatelessWidget {
const AppCard({
super.key,
required this.child,
this.title,
this.subtitle,
this.padding = const EdgeInsets.all(AppSpacing.s4),
this.margin = const EdgeInsets.symmetric(horizontal: AppSpacing.gutter),
this.onTap,
});
final Widget child;
final String? title;
final String? subtitle;
final EdgeInsetsGeometry padding;
final EdgeInsetsGeometry margin;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final card = Container(
margin: margin,
padding: padding,
decoration: BoxDecoration(
color: AppColors.of(context, AppColors.groupedSurface),
borderRadius: BorderRadius.circular(AppRadii.lg),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null) AppText.headline(title!),
if (subtitle != null) ...[
const SizedBox(height: 2),
AppText.footnote(subtitle!),
],
if (title != null || subtitle != null) const SizedBox(height: AppSpacing.s3),
child,
],
),
);
if (onTap == null) return card;
return CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
onPressed: onTap,
child: card,
);
}
}
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/cupertino.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:please_pay_me/theme/tokens.dart';
/// Capsule tag / filter pill (iOS has no Material chip — this is the HIG-ish
/// equivalent used in Photos & Mail filters).
class AppChip extends StatelessWidget {
const AppChip({
super.key,
required this.label,
this.selected = false,
this.icon,
this.onPressed,
});
final String label;
final bool selected;
final IconData? icon;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
final accent = AppColors.of(context, AppColors.accent);
final background = selected ? accent : AppColors.of(context, AppColors.systemGray5);
final foreground = selected ? const Color(0xFFFFFFFF) : AppColors.of(context, AppColors.label);
return CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
onPressed: onPressed,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.s3,
vertical: AppSpacing.s2 - 1,
),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(AppRadii.capsule),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 15, color: foreground),
const SizedBox(width: AppSpacing.s1 + 2),
],
Text(
label,
style: GoogleFonts.inter(
textStyle: AppTypography.subhead.copyWith(fontWeight: FontWeight.w500),
color: foreground,
),
),
],
),
),
);
}
}
@@ -0,0 +1,66 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/tokens.dart';
import 'package:please_pay_me/ui/atoms/app_text.dart';
/// Inset-grouped section: rounded surface + hairline separators between rows.
class AppListSection extends StatelessWidget {
const AppListSection({
super.key,
required this.children,
this.header,
this.footer,
this.separatorIndent = 16,
this.margin = const EdgeInsets.symmetric(horizontal: AppSpacing.gutter),
});
final List<Widget> children;
final String? header;
final String? footer;
final double separatorIndent;
final EdgeInsetsGeometry margin;
@override
Widget build(BuildContext context) {
final rows = <Widget>[];
for (var i = 0; i < children.length; i++) {
rows.add(children[i]);
if (i < children.length - 1) {
rows.add(
Padding(
padding: EdgeInsets.only(left: separatorIndent),
child: Container(
height: AppSizes.hairline,
color: AppColors.of(context, AppColors.opaqueSeparator),
),
),
);
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (header != null) AppSectionHeader(header!),
Container(
margin: margin,
decoration: BoxDecoration(
color: AppColors.of(context, AppColors.groupedSurface),
borderRadius: BorderRadius.circular(AppRadii.lg),
),
clipBehavior: Clip.antiAlias,
child: Column(children: rows),
),
if (footer != null)
Padding(
padding: const EdgeInsets.fromLTRB(
AppSpacing.gutter + 4,
AppSpacing.s2,
AppSpacing.gutter + 4,
0,
),
child: AppText.footnote(footer!),
),
],
);
}
}
+110
View File
@@ -0,0 +1,110 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/tokens.dart';
import 'package:please_pay_me/ui/atoms/app_text.dart';
/// Single row of an inset-grouped list (Settings-style).
class AppListTile extends StatefulWidget {
const AppListTile({
super.key,
required this.title,
this.subtitle,
this.leading,
this.value,
this.trailing,
this.onTap,
this.showChevron = true,
this.destructive = false,
});
final String title;
final String? subtitle;
final Widget? leading;
/// Secondary gray text aligned to the right (iOS `additionalInfo`).
final String? value;
/// Custom trailing widget — replaces [value] and the chevron.
final Widget? trailing;
final VoidCallback? onTap;
final bool showChevron;
final bool destructive;
@override
State<AppListTile> createState() => _AppListTileState();
}
class _AppListTileState extends State<AppListTile> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
final tappable = widget.onTap != null;
final row = Container(
color: _pressed
? AppColors.of(context, AppColors.systemGray5)
: AppColors.of(context, AppColors.groupedSurface),
constraints: const BoxConstraints(minHeight: AppSizes.rowMinHeight),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.gutter,
vertical: AppSpacing.s2 + 2,
),
child: Row(
children: [
if (widget.leading != null) ...[
widget.leading!,
const SizedBox(width: AppSpacing.s3),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AppText.body(
widget.title,
color: widget.destructive ? AppColors.systemRed : AppColors.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (widget.subtitle != null) ...[
const SizedBox(height: 2),
AppText.footnote(widget.subtitle!, maxLines: 2, overflow: TextOverflow.ellipsis),
],
],
),
),
if (widget.trailing != null)
widget.trailing!
else ...[
if (widget.value != null)
Padding(
padding: const EdgeInsets.only(left: AppSpacing.s2),
child: AppText.body(widget.value!, color: AppColors.secondaryLabel),
),
if (tappable && widget.showChevron)
Padding(
padding: const EdgeInsets.only(left: AppSpacing.s1),
child: Icon(
CupertinoIcons.chevron_forward,
size: 16,
color: AppColors.of(context, AppColors.tertiaryLabel),
),
),
],
],
),
);
if (!tappable) return row;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: widget.onTap,
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
child: row,
);
}
}
@@ -0,0 +1,36 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/tokens.dart';
import 'package:please_pay_me/ui/molecules/app_list_tile.dart';
/// Settings row with a native iOS switch on the trailing edge.
class AppSwitchRow extends StatelessWidget {
const AppSwitchRow({
super.key,
required this.title,
required this.value,
required this.onChanged,
this.subtitle,
this.leading,
});
final String title;
final String? subtitle;
final Widget? leading;
final bool value;
final ValueChanged<bool>? onChanged;
@override
Widget build(BuildContext context) {
return AppListTile(
title: title,
subtitle: subtitle,
leading: leading,
showChevron: false,
trailing: CupertinoSwitch(
value: value,
onChanged: onChanged,
activeTrackColor: AppColors.of(context, AppColors.accent),
),
);
}
}