feat(proj): init
This commit is contained in:
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user