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
+107
View File
@@ -0,0 +1,107 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/ui/atoms/app_text.dart';
/// Native iOS alert.
class AppAlert extends StatelessWidget {
const AppAlert({
super.key,
required this.title,
this.message,
this.confirmLabel = 'OK',
this.cancelLabel,
this.destructive = false,
});
final String title;
final String? message;
final String confirmLabel;
final String? cancelLabel;
final bool destructive;
@override
Widget build(BuildContext context) {
return CupertinoAlertDialog(
title: AppText.headline(title),
content: message == null
? null
: Padding(
padding: const EdgeInsets.only(top: 6),
child: AppText.subhead(message!, textAlign: TextAlign.center),
),
actions: [
if (cancelLabel != null)
CupertinoDialogAction(
onPressed: () => Navigator.of(context).pop(false),
child: Text(cancelLabel!),
),
CupertinoDialogAction(
isDefaultAction: !destructive,
isDestructiveAction: destructive,
onPressed: () => Navigator.of(context).pop(true),
child: Text(confirmLabel),
),
],
);
}
}
Future<bool?> showAppAlert({
required BuildContext context,
required String title,
String? message,
String confirmLabel = 'OK',
String? cancelLabel,
bool destructive = false,
}) {
return showCupertinoDialog<bool>(
context: context,
builder: (_) => AppAlert(
title: title,
message: message,
confirmLabel: confirmLabel,
cancelLabel: cancelLabel,
destructive: destructive,
),
);
}
class AppActionSheetAction {
const AppActionSheetAction({
required this.label,
this.destructive = false,
this.isDefault = false,
});
final String label;
final bool destructive;
final bool isDefault;
}
Future<int?> showAppActionSheet({
required BuildContext context,
String? title,
String? message,
required List<AppActionSheetAction> actions,
String cancelLabel = 'Отмена',
}) {
return showCupertinoModalPopup<int>(
context: context,
builder: (ctx) => CupertinoActionSheet(
title: title == null ? null : Text(title),
message: message == null ? null : Text(message),
actions: [
for (var i = 0; i < actions.length; i++)
CupertinoActionSheetAction(
isDestructiveAction: actions[i].destructive,
isDefaultAction: actions[i].isDefault,
onPressed: () => Navigator.of(ctx).pop(i),
child: Text(actions[i].label),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(cancelLabel),
),
),
);
}
+86
View File
@@ -0,0 +1,86 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/tokens.dart';
import 'package:please_pay_me/ui/atoms/app_text.dart';
/// Standard iOS navigation bar (44pt) with optional subtitle line.
class AppNavBar extends StatelessWidget implements ObstructingPreferredSizeWidget {
const AppNavBar({
super.key,
required this.title,
this.subtitle,
this.leading,
this.trailing,
this.previousPageTitle,
this.transparent = false,
});
final String title;
final String? subtitle;
final Widget? leading;
final Widget? trailing;
final String? previousPageTitle;
final bool transparent;
@override
Size get preferredSize => const Size.fromHeight(44);
@override
bool shouldFullyObstruct(BuildContext context) => !transparent;
@override
Widget build(BuildContext context) {
return CupertinoNavigationBar(
leading: leading,
trailing: trailing,
previousPageTitle: previousPageTitle,
backgroundColor: transparent
? const Color(0x00000000)
: AppColors.of(context, AppColors.barBackground),
border: transparent
? null
: Border(
bottom: BorderSide(
color: AppColors.of(context, AppColors.separator),
width: AppSizes.hairline,
),
),
middle: subtitle == null
? AppText.headline(title)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppText.headline(title, maxLines: 1, overflow: TextOverflow.ellipsis),
AppText.caption(
subtitle!,
color: AppColors.secondaryLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}
/// Large-title bar for the root of a scrollable screen.
class AppLargeNavBar extends StatelessWidget {
const AppLargeNavBar({super.key, required this.title, this.trailing});
final String title;
final Widget? trailing;
@override
Widget build(BuildContext context) {
return CupertinoSliverNavigationBar(
largeTitle: AppText.largeTitle(title),
trailing: trailing,
backgroundColor: AppColors.of(context, AppColors.barBackground),
border: Border(
bottom: BorderSide(
color: AppColors.of(context, AppColors.separator),
width: AppSizes.hairline,
),
),
);
}
}
+70
View File
@@ -0,0 +1,70 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/tokens.dart';
import 'package:please_pay_me/ui/atoms/app_button.dart';
/// Bottom sheet with a native wheel date picker.
Future<DateTime?> showAppDatePicker({
required BuildContext context,
required DateTime initialDate,
DateTime? minimumDate,
DateTime? maximumDate,
String confirmLabel = 'Готово',
}) {
var selected = initialDate;
return showCupertinoModalPopup<DateTime>(
context: context,
builder: (ctx) => Container(
height: 320,
padding: const EdgeInsets.only(top: AppSpacing.s2),
color: AppColors.of(ctx, AppColors.groupedSurface),
child: SafeArea(
top: false,
child: Column(
children: [
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: initialDate,
minimumDate: minimumDate,
maximumDate: maximumDate,
onDateTimeChanged: (value) => selected = value,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(
AppSpacing.gutter,
AppSpacing.s2,
AppSpacing.gutter,
AppSpacing.s3,
),
child: AppButton(
label: confirmLabel,
onPressed: () => Navigator.of(ctx).pop(
DateTime(selected.year, selected.month, selected.day),
),
),
),
],
),
),
),
);
}
/// Full-height modal used for create/edit forms.
Future<T?> showAppFormSheet<T>({
required BuildContext context,
required WidgetBuilder builder,
}) {
return showCupertinoModalPopup<T>(
context: context,
builder: (ctx) => Padding(
padding: EdgeInsets.only(top: MediaQuery.of(ctx).padding.top + AppSpacing.s6),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(AppRadii.xl)),
child: Builder(builder: builder),
),
),
);
}
@@ -0,0 +1,54 @@
import 'package:flutter/cupertino.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:please_pay_me/theme/tokens.dart';
/// iOS sliding segmented control — the native alternative to tabs.
class AppSegmentedControl extends StatelessWidget {
const AppSegmentedControl({
super.key,
required this.labels,
required this.index,
required this.onChanged,
this.padding = const EdgeInsets.symmetric(horizontal: AppSpacing.gutter),
});
final List<String> labels;
final int index;
final ValueChanged<int> onChanged;
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
return Padding(
padding: padding,
child: SizedBox(
width: double.infinity,
child: CupertinoSlidingSegmentedControl<int>(
groupValue: index.clamp(0, labels.length - 1),
backgroundColor: AppColors.of(context, AppColors.systemGray5),
thumbColor: AppColors.of(context, AppColors.groupedSurface),
onValueChanged: (value) {
if (value != null) onChanged(value);
},
children: {
for (var i = 0; i < labels.length; i++)
i: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Text(
labels[i],
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.inter(
textStyle: AppTypography.subhead.copyWith(
fontWeight: i == index ? FontWeight.w600 : FontWeight.w400,
),
color: AppColors.of(context, AppColors.label),
),
),
),
},
),
),
);
}
}
+38
View File
@@ -0,0 +1,38 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/tokens.dart';
class AppTabItem {
const AppTabItem({required this.icon, required this.label, this.activeIcon});
final IconData icon;
final IconData? activeIcon;
final String label;
}
/// Bottom tab bar (iOS): 49pt, hairline top border, tint = brand accent.
///
/// Extends [CupertinoTabBar] so it can be passed to [CupertinoTabScaffold];
/// dynamic colors are resolved by the base class against the active theme.
class AppTabBar extends CupertinoTabBar {
AppTabBar({
super.key,
required List<AppTabItem> items,
required super.currentIndex,
required ValueChanged<int> super.onTap,
}) : super(
items: [
for (final item in items)
BottomNavigationBarItem(
icon: Icon(item.icon),
activeIcon: item.activeIcon == null ? null : Icon(item.activeIcon),
label: item.label,
),
],
activeColor: AppColors.accent,
inactiveColor: AppColors.systemGray,
backgroundColor: AppColors.barBackground,
border: const Border(
top: BorderSide(color: AppColors.separator, width: AppSizes.hairline),
),
);
}