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
@@ -0,0 +1,11 @@
import 'package:widgetbook/widgetbook.dart';
/// Common phone frames for visual QA (ViewportAddon replaces DeviceFrameAddon).
ViewportAddon buildDeviceFrameAddon() {
return ViewportAddon([
Viewports.none,
IosViewports.iPhone13,
IosViewports.iPhone13Mini,
AndroidViewports.samsungGalaxyS20,
]);
}
@@ -0,0 +1,19 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:widgetbook/widgetbook.dart';
/// Locale switcher: Russian / English.
LocalizationAddon buildLocaleAddon() {
return LocalizationAddon(
locales: const [
Locale('ru'),
Locale('en'),
],
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
initialLocale: const Locale('ru'),
);
}
@@ -0,0 +1,12 @@
import 'package:please_pay_me/theme/theme.dart';
import 'package:widgetbook/widgetbook.dart';
/// Light / dark Cupertino theme built from the shared product tokens.
CupertinoThemeAddon buildThemeAddon() {
return CupertinoThemeAddon(
themes: [
WidgetbookTheme(name: 'Light', data: buildLightTheme()),
WidgetbookTheme(name: 'Dark', data: buildDarkTheme()),
],
);
}
+26
View File
@@ -0,0 +1,26 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me_widgetbook/addons/device_frame_addon.dart';
import 'package:please_pay_me_widgetbook/addons/locale_addon.dart';
import 'package:please_pay_me_widgetbook/addons/theme_addon.dart';
import 'package:please_pay_me_widgetbook/catalog.dart';
import 'package:widgetbook/widgetbook.dart';
/// Entry Widgetbook application — portable across projects that depend on the
/// same UI package (`please_pay_me`).
class WidgetbookRoot extends StatelessWidget {
const WidgetbookRoot({super.key});
@override
Widget build(BuildContext context) {
return Widgetbook.cupertino(
directories: buildCatalogDirectories(),
addons: [
buildThemeAddon(),
buildLocaleAddon(),
buildDeviceFrameAddon(),
InspectorAddon(),
AlignmentAddon(initialAlignment: Alignment.center),
],
);
}
}
+75
View File
@@ -0,0 +1,75 @@
import 'package:please_pay_me_widgetbook/use_cases/atoms/buttons_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/atoms/icons_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/atoms/text_fields_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/atoms/typography_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/feedback/progress_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/feedback/skeletons_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/feedback/toasts_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/molecules/avatars_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/molecules/cards_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/molecules/chips_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/molecules/list_tiles_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/molecules/switches_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/navigation/dialogs_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/navigation/nav_bar_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/navigation/segmented_control_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/navigation/tab_bar_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/screens/app_screens_use_cases.dart';
import 'package:please_pay_me_widgetbook/use_cases/screens/sheets_use_cases.dart';
import 'package:widgetbook/widgetbook.dart';
/// Single source of directory tree for Widgetbook + smoke tests.
List<WidgetbookNode> buildCatalogDirectories() {
return [
WidgetbookFolder(
name: 'Atoms',
children: [
buttonsComponent(),
textFieldsComponent(),
typographyComponent(),
iconsComponent(),
],
),
WidgetbookFolder(
name: 'Molecules',
children: [
cardsComponent(),
listTilesComponent(),
switchesComponent(),
chipsComponent(),
avatarsComponent(),
],
),
WidgetbookFolder(
name: 'Navigation',
children: [
navBarComponent(),
tabBarComponent(),
segmentedControlComponent(),
dialogsComponent(),
],
),
WidgetbookFolder(
name: 'Feedback',
children: [
progressComponent(),
skeletonsComponent(),
toastsComponent(),
],
),
WidgetbookFolder(
name: 'Screens',
children: [
overviewScreenComponent(),
journalScreenComponent(),
budgetsScreenComponent(),
workScreenComponent(),
profileScreenComponent(),
splashScreenComponent(),
loginScreenComponent(),
sheetsComponent(),
homeTabsComponent(),
],
),
];
}
@@ -0,0 +1,93 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:widgetbook/widgetbook.dart';
String knobText(
BuildContext context, {
required String label,
String initialValue = 'Label',
}) {
return context.knobs.string(label: label, initialValue: initialValue);
}
bool knobBool(
BuildContext context, {
required String label,
bool initialValue = false,
}) {
return context.knobs.boolean(label: label, initialValue: initialValue);
}
double knobDouble(
BuildContext context, {
required String label,
double initialValue = 0.5,
double min = 0,
double max = 1,
}) {
return context.knobs.double.slider(
label: label,
initialValue: initialValue,
min: min,
max: max,
);
}
int knobInt(
BuildContext context, {
required String label,
int initialValue = 0,
int min = 0,
int max = 10,
}) {
return context.knobs.int.slider(
label: label,
initialValue: initialValue,
min: min,
max: max,
);
}
Color knobColor(
BuildContext context, {
required String label,
Color? initialValue,
}) {
return context.knobs.color(
label: label,
initialValue: initialValue ?? AppColors.of(context, AppColors.accent),
);
}
/// Dropdown over iOS system tints used by badges and progress bars.
Color knobTint(BuildContext context, {String label = 'Tint'}) {
const options = <String, Color>{
'accent': AppColors.accent,
'red': AppColors.systemRed,
'orange': AppColors.systemOrange,
'green': AppColors.systemGreen,
'gray': AppColors.systemGray,
};
final key = context.knobs.object.dropdown(
label: label,
options: options.keys.toList(),
initialOption: 'accent',
);
return options[key]!;
}
T knobEnum<T extends Enum>(
BuildContext context, {
required String label,
required List<T> values,
required T initial,
}) {
return context.knobs.object.dropdown(
label: label,
options: values,
labelBuilder: (value) => value.name,
initialOption: initial,
);
}
@@ -0,0 +1,46 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:widgetbook/widgetbook.dart';
enum UiAsyncState { content, loading, empty, error }
UiAsyncState knobAsyncState(BuildContext context, {String label = 'State'}) {
return context.knobs.object.dropdown(
label: label,
options: UiAsyncState.values,
labelBuilder: (value) => value.name,
initialOption: UiAsyncState.content,
);
}
/// Shared empty / error / loading wrappers for molecules & screens.
Widget wrapAsyncState({
required BuildContext context,
required UiAsyncState state,
required Widget Function() builder,
String emptyLabel = 'Пока пусто',
String errorLabel = 'Что-то пошло не так',
}) {
return switch (state) {
UiAsyncState.loading => const Padding(
padding: EdgeInsets.all(AppSpacing.s6),
child: Center(child: AppSpinner()),
),
UiAsyncState.empty => Padding(
padding: const EdgeInsets.all(AppSpacing.s6),
child: Center(child: AppText.subhead(emptyLabel)),
),
UiAsyncState.error => Padding(
padding: const EdgeInsets.all(AppSpacing.s6),
child: Center(
child: AppText.subhead(
knobText(context, label: 'Error text', initialValue: errorLabel),
color: AppColors.systemRed,
),
),
),
UiAsyncState.content => builder(),
};
}
+10
View File
@@ -0,0 +1,10 @@
import 'package:flutter/widgets.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:please_pay_me_widgetbook/app.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting('ru');
runApp(const WidgetbookRoot());
}
@@ -0,0 +1,76 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/core/config/app_config.dart';
import 'package:please_pay_me/core/storage/session_storage.dart';
import 'package:please_pay_me/data/demo/demo_backend.dart';
import 'package:please_pay_me/features/auth/session_controller.dart';
import 'package:please_pay_me/features/budgets/budgets_controller.dart';
import 'package:please_pay_me/features/journal/journal_controller.dart';
import 'package:please_pay_me/features/work/jobs_controller.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:provider/provider.dart';
/// Wires real app screens to the in-memory [DemoBackend] so the catalog shows
/// production widgets with production state management.
class DemoScope extends StatefulWidget {
const DemoScope({super.key, required this.child, this.seeded = true});
final Widget child;
/// `false` renders the empty states (no budgets / jobs / operations).
final bool seeded;
@override
State<DemoScope> createState() => _DemoScopeState();
}
class _DemoScopeState extends State<DemoScope> {
late DemoBackend _backend;
late SessionController _session;
@override
void initState() {
super.initState();
_build();
}
@override
void didUpdateWidget(DemoScope oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.seeded != widget.seeded) _build();
}
void _build() {
_backend = widget.seeded ? DemoBackend() : DemoBackend.empty();
_session = SessionController(
config: AppConfig.demo,
storage: InMemorySessionStorage(),
demoBackend: _backend,
)..startDemo();
}
@override
Widget build(BuildContext context) {
return MultiProvider(
key: ValueKey(widget.seeded),
providers: [
ChangeNotifierProvider<SessionController>.value(value: _session),
ChangeNotifierProvider<ThemeController>(
create: (_) => ThemeController(store: MemoryThemeStore()),
),
ChangeNotifierProvider(
create: (_) => BudgetsController(
budgets: _backend.budgets,
expenses: _backend.expenses,
)..load(),
),
ChangeNotifierProvider(
create: (_) => JournalController(expenses: _backend.expenses)..load(),
),
ChangeNotifierProvider(
create: (_) => JobsController(jobs: _backend.jobs)..load(),
),
],
child: widget.child,
);
}
}
@@ -0,0 +1,56 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
/// Puts a use-case on the iOS grouped background so inset-grouped cards and
/// separators read the way they do inside the app.
class IosPreview extends StatelessWidget {
const IosPreview({
super.key,
required this.child,
this.padding = const EdgeInsets.symmetric(vertical: AppSpacing.s5),
this.maxWidth = 420,
this.stretch = false,
});
/// Preview for controls that should not be stretched (buttons, chips).
const IosPreview.centered({super.key, required this.child})
: padding = const EdgeInsets.all(AppSpacing.s5),
maxWidth = 420,
stretch = false;
final Widget child;
final EdgeInsetsGeometry padding;
final double maxWidth;
final bool stretch;
@override
Widget build(BuildContext context) {
return ColoredBox(
color: AppColors.of(context, AppColors.groupedBackground),
child: Center(
child: SingleChildScrollView(
padding: padding,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Column(
crossAxisAlignment:
stretch ? CrossAxisAlignment.stretch : CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [child],
),
),
),
),
);
}
}
/// Full-screen preview (screens already bring their own scaffold).
class IosScreenPreview extends StatelessWidget {
const IosScreenPreview({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) => child;
}
@@ -0,0 +1,80 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent buttonsComponent() {
return WidgetbookComponent(
name: 'AppButton',
useCases: [
WidgetbookUseCase(
name: 'Playground',
builder: (context) {
return IosPreview(
stretch: true,
child: AppButton(
label: knobText(context, label: 'Label', initialValue: 'Сохранить'),
style: knobEnum(
context,
label: 'Style',
values: AppButtonStyle.values,
initial: AppButtonStyle.filled,
),
size: knobEnum(
context,
label: 'Size',
values: AppButtonSize.values,
initial: AppButtonSize.large,
),
expanded: knobBool(context, label: 'Expanded', initialValue: true),
loading: knobBool(context, label: 'Loading'),
icon: knobBool(context, label: 'Icon') ? CupertinoIcons.plus : null,
onPressed:
knobBool(context, label: 'Enabled', initialValue: true) ? () {} : null,
),
);
},
),
WidgetbookUseCase(
name: 'All styles',
builder: (context) {
return IosPreview(
stretch: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (final style in AppButtonStyle.values) ...[
AppButton(label: style.name, style: style, onPressed: () {}),
const SizedBox(height: AppSpacing.s3),
],
],
),
);
},
),
WidgetbookUseCase(
name: 'Sizes',
builder: (context) {
return IosPreview.centered(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final size in AppButtonSize.values) ...[
AppButton(
label: 'Размер ${size.name}',
size: size,
expanded: false,
onPressed: () {},
),
const SizedBox(height: AppSpacing.s3),
],
],
),
);
},
),
],
);
}
@@ -0,0 +1,68 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
const _icons = <IconData>[
CupertinoIcons.house_fill,
CupertinoIcons.cart_fill,
CupertinoIcons.bag_fill,
CupertinoIcons.money_rubl_circle_fill,
CupertinoIcons.chart_pie_fill,
CupertinoIcons.calendar,
CupertinoIcons.bell_fill,
CupertinoIcons.gear_alt_fill,
CupertinoIcons.person_fill,
CupertinoIcons.creditcard_fill,
];
WidgetbookComponent iconsComponent() {
return WidgetbookComponent(
name: 'Icons',
useCases: [
WidgetbookUseCase(
name: 'Glyphs',
builder: (context) {
final size = knobDouble(context, label: 'Size', initialValue: 24, min: 12, max: 48);
final tint = knobTint(context);
return IosPreview(
child: Wrap(
spacing: AppSpacing.s5,
runSpacing: AppSpacing.s5,
alignment: WrapAlignment.center,
children: [
for (final icon in _icons) AppIcon(icon, size: size, color: tint),
],
),
);
},
),
WidgetbookUseCase(
name: 'Badges',
builder: (context) {
return IosPreview(
child: Wrap(
spacing: AppSpacing.s4,
runSpacing: AppSpacing.s4,
alignment: WrapAlignment.center,
children: [
for (final (index, icon) in _icons.indexed)
AppIconBadge(
icon: icon,
color: switch (index % 4) {
0 => AppColors.accent,
1 => AppColors.systemRed,
2 => AppColors.systemOrange,
_ => AppColors.systemGray,
},
),
],
),
);
},
),
],
);
}
@@ -0,0 +1,62 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent textFieldsComponent() {
return WidgetbookComponent(
name: 'AppTextField',
useCases: [
WidgetbookUseCase(
name: 'Playground',
builder: (context) {
final error = knobText(context, label: 'Error', initialValue: '');
return IosPreview(
stretch: true,
padding: const EdgeInsets.all(AppSpacing.s4),
child: AppTextField(
label: knobText(context, label: 'Label', initialValue: 'Сумма'),
placeholder: knobText(context, label: 'Placeholder', initialValue: '0 ₽'),
enabled: knobBool(context, label: 'Enabled', initialValue: true),
obscureText: knobBool(context, label: 'Obscure'),
clearable: knobBool(context, label: 'Clear button', initialValue: true),
prefixIcon: knobBool(context, label: 'Prefix icon')
? CupertinoIcons.search
: null,
errorText: error.isEmpty ? null : error,
),
);
},
),
WidgetbookUseCase(
name: 'Form group',
builder: (context) {
return const IosPreview(
stretch: true,
padding: EdgeInsets.all(AppSpacing.s4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AppTextField(label: 'Название', placeholder: 'Продукты'),
SizedBox(height: AppSpacing.s4),
AppTextField(
label: 'Сумма',
placeholder: '0 ₽',
keyboardType: TextInputType.number,
),
SizedBox(height: AppSpacing.s4),
AppTextField(
label: 'Комментарий',
placeholder: 'Необязательно',
errorText: 'Слишком длинный комментарий',
),
],
),
);
},
),
],
);
}
@@ -0,0 +1,63 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent typographyComponent() {
return WidgetbookComponent(
name: 'Typography',
useCases: [
WidgetbookUseCase(
name: 'iOS scale',
builder: (context) {
final sample = knobText(context, label: 'Sample', initialValue: 'Бюджет на неделю');
return IosPreview(
stretch: true,
padding: const EdgeInsets.all(AppSpacing.s4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppText.largeTitle(sample),
const SizedBox(height: AppSpacing.s3),
AppText.title(sample),
const SizedBox(height: AppSpacing.s3),
AppText.headline(sample),
const SizedBox(height: AppSpacing.s3),
AppText.body(sample),
const SizedBox(height: AppSpacing.s3),
AppText.callout(sample),
const SizedBox(height: AppSpacing.s3),
AppText.subhead(sample),
const SizedBox(height: AppSpacing.s3),
AppText.footnote(sample),
const SizedBox(height: AppSpacing.s3),
AppText.caption(sample),
],
),
);
},
),
WidgetbookUseCase(
name: 'Section header',
builder: (context) {
return IosPreview(
stretch: true,
child: AppListSection(
header: knobText(context, label: 'Header', initialValue: 'Настройки'),
footer: knobText(
context,
label: 'Footer',
initialValue: 'Пояснение под группой, как в iOS Settings.',
),
children: const [
AppListTile(title: 'Строка', value: 'Значение'),
],
),
);
},
),
],
);
}
@@ -0,0 +1,48 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent progressComponent() {
return WidgetbookComponent(
name: 'Progress',
useCases: [
WidgetbookUseCase(
name: 'Activity indicator',
builder: (context) {
return IosPreview.centered(
child: AppSpinner(
radius: knobDouble(context, label: 'Radius', initialValue: 14, min: 6, max: 28),
color: knobTint(context),
),
);
},
),
WidgetbookUseCase(
name: 'Progress bar',
builder: (context) {
return IosPreview(
stretch: true,
padding: const EdgeInsets.all(AppSpacing.s4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AppProgressBar(
value: knobDouble(context, label: 'Value', initialValue: 0.42),
color: knobTint(context),
height: knobDouble(context, label: 'Height', initialValue: 4, min: 2, max: 12),
),
const SizedBox(height: AppSpacing.s4),
const AppProgressBar(value: 0.15, color: AppColors.systemRed),
const SizedBox(height: AppSpacing.s4),
const AppProgressBar(value: 1, color: AppColors.systemGreen),
],
),
);
},
),
],
);
}
@@ -0,0 +1,38 @@
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent skeletonsComponent() {
return WidgetbookComponent(
name: 'AppSkeleton',
useCases: [
WidgetbookUseCase(
name: 'Block',
builder: (context) {
return IosPreview.centered(
child: AppSkeleton(
width: knobDouble(context, label: 'Width', initialValue: 200, min: 40, max: 320),
height: knobDouble(context, label: 'Height', initialValue: 16, min: 8, max: 80),
radius: knobDouble(context, label: 'Radius', initialValue: 6, min: 0, max: 24),
),
);
},
),
WidgetbookUseCase(
name: 'List loading',
builder: (context) {
final rows = knobInt(context, label: 'Rows', initialValue: 4, min: 1, max: 8);
return IosPreview(
stretch: true,
child: AppListSection(
header: 'Загрузка',
separatorIndent: 60,
children: List.generate(rows, (_) => const AppSkeletonRow()),
),
);
},
),
],
);
}
@@ -0,0 +1,46 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent toastsComponent() {
return WidgetbookComponent(
name: 'AppToast',
useCases: [
WidgetbookUseCase(
name: 'Static',
builder: (context) {
return IosPreview.centered(
child: AppToast(
message: knobText(context, label: 'Message', initialValue: 'Трата сохранена'),
tint: knobTint(context),
icon: knobBool(context, label: 'Icon', initialValue: true)
? CupertinoIcons.check_mark_circled_solid
: null,
),
);
},
),
WidgetbookUseCase(
name: 'Trigger',
builder: (context) {
return IosPreview(
stretch: true,
child: AppButton(
label: 'Показать toast',
onPressed: () => showAppToast(
context,
message: knobText(
context,
label: 'Message',
initialValue: 'Трата сохранена',
),
),
),
);
},
),
],
);
}
@@ -0,0 +1,48 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent avatarsComponent() {
return WidgetbookComponent(
name: 'AppAvatar',
useCases: [
WidgetbookUseCase(
name: 'Playground',
builder: (context) {
final initials = knobText(context, label: 'Initials', initialValue: 'ВА');
return IosPreview.centered(
child: AppAvatar(
initials: knobBool(context, label: 'Use initials', initialValue: true)
? initials
: null,
radius: knobDouble(context, label: 'Radius', initialValue: 32, min: 12, max: 56),
),
);
},
),
WidgetbookUseCase(
name: 'Sizes',
builder: (context) {
return const IosPreview.centered(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
AppAvatar(initials: 'А', radius: 16),
SizedBox(width: AppSpacing.s4),
AppAvatar(initials: 'ВА', radius: 22),
SizedBox(width: AppSpacing.s4),
AppAvatar(radius: 32),
SizedBox(width: AppSpacing.s4),
AppAvatar(initials: 'ППМ', radius: 44),
],
),
);
},
),
],
);
}
@@ -0,0 +1,72 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent cardsComponent() {
return WidgetbookComponent(
name: 'AppCard',
useCases: [
WidgetbookUseCase(
name: 'Playground',
builder: (context) {
final subtitle = knobText(context, label: 'Subtitle', initialValue: 'До 25 сентября');
return IosPreview(
stretch: true,
child: AppCard(
title: knobText(context, label: 'Title', initialValue: 'Бюджет'),
subtitle: subtitle.isEmpty ? null : subtitle,
onTap: knobBool(context, label: 'Tappable') ? () {} : null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppText.largeTitle(
knobText(context, label: 'Amount', initialValue: '18 420 ₽'),
),
const SizedBox(height: AppSpacing.s4),
AppProgressBar(
value: knobDouble(context, label: 'Progress', initialValue: 0.42),
),
],
),
),
);
},
),
WidgetbookUseCase(
name: 'Metrics row',
builder: (context) {
return const IosPreview(
stretch: true,
child: AppCard(
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppText.footnote('Доход'),
AppText.title('64 000 ₽'),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppText.footnote('Расход'),
AppText.title('45 580 ₽', color: AppColors.systemRed),
],
),
),
],
),
),
);
},
),
],
);
}
@@ -0,0 +1,45 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent chipsComponent() {
return WidgetbookComponent(
name: 'AppChip',
useCases: [
WidgetbookUseCase(
name: 'Playground',
builder: (context) {
return IosPreview.centered(
child: AppChip(
label: knobText(context, label: 'Label', initialValue: 'Еда'),
selected: knobBool(context, label: 'Selected'),
icon: knobBool(context, label: 'Icon') ? CupertinoIcons.tag_fill : null,
onPressed: () {},
),
);
},
),
WidgetbookUseCase(
name: 'Filter row',
builder: (context) {
final selected = knobInt(context, label: 'Selected index', min: 0, max: 3);
const labels = ['Все', 'Еда', 'Транспорт', 'Подписки'];
return IosPreview.centered(
child: Wrap(
spacing: AppSpacing.s2,
runSpacing: AppSpacing.s2,
alignment: WrapAlignment.center,
children: [
for (final (index, label) in labels.indexed)
AppChip(label: label, selected: index == selected, onPressed: () {}),
],
),
);
},
),
],
);
}
@@ -0,0 +1,80 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/knobs/state_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent listTilesComponent() {
return WidgetbookComponent(
name: 'AppListTile',
useCases: [
WidgetbookUseCase(
name: 'Playground',
builder: (context) {
final subtitle = knobText(context, label: 'Subtitle', initialValue: 'Супермаркет');
final value = knobText(context, label: 'Value', initialValue: '1 840 ₽');
return IosPreview(
stretch: true,
child: AppListSection(
separatorIndent: 60,
children: [
AppListTile(
leading: knobBool(context, label: 'Leading badge', initialValue: true)
? const AppIconBadge(icon: CupertinoIcons.cart_fill)
: null,
title: knobText(context, label: 'Title', initialValue: 'Продукты'),
subtitle: subtitle.isEmpty ? null : subtitle,
value: value.isEmpty ? null : value,
destructive: knobBool(context, label: 'Destructive'),
showChevron: knobBool(context, label: 'Chevron', initialValue: true),
onTap: knobBool(context, label: 'Tappable', initialValue: true) ? () {} : null,
),
],
),
);
},
),
WidgetbookUseCase(
name: 'Grouped section',
builder: (context) {
final state = knobAsyncState(context);
return IosPreview(
stretch: true,
child: AppListSection(
header: 'Сегодня',
footer: 'Свайп по строке открывает действия.',
separatorIndent: 60,
children: [
wrapAsyncState(
context: context,
state: state,
builder: () => const Column(
children: [
AppListTile(
leading: AppIconBadge(icon: CupertinoIcons.cart_fill),
title: 'Продукты',
subtitle: 'Супермаркет',
value: '1 840 ₽',
),
AppListTile(
leading: AppIconBadge(
icon: CupertinoIcons.car_fill,
color: AppColors.systemOrange,
),
title: 'Такси',
subtitle: 'Транспорт',
value: '420 ₽',
),
],
),
),
],
),
);
},
),
],
);
}
@@ -0,0 +1,75 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent switchesComponent() {
return WidgetbookComponent(
name: 'AppSwitchRow',
useCases: [
WidgetbookUseCase(
name: 'Playground',
builder: (context) {
final subtitle = knobText(context, label: 'Subtitle', initialValue: '');
return IosPreview(
stretch: true,
child: AppListSection(
separatorIndent: 60,
children: [
AppSwitchRow(
leading: knobBool(context, label: 'Leading badge', initialValue: true)
? const AppIconBadge(
icon: CupertinoIcons.bell_fill,
color: AppColors.systemRed,
)
: null,
title: knobText(context, label: 'Title', initialValue: 'Уведомления'),
subtitle: subtitle.isEmpty ? null : subtitle,
value: knobBool(context, label: 'Value', initialValue: true),
onChanged: knobBool(context, label: 'Enabled', initialValue: true)
? (_) {}
: null,
),
],
),
);
},
),
WidgetbookUseCase(
name: 'Settings group',
builder: (context) {
return IosPreview(
stretch: true,
child: AppListSection(
header: 'Настройки',
footer: 'Push приходят за час до дедлайна бюджета.',
separatorIndent: 60,
children: [
AppSwitchRow(
leading: const AppIconBadge(
icon: CupertinoIcons.bell_fill,
color: AppColors.systemRed,
),
title: 'Уведомления',
value: true,
onChanged: (_) {},
),
AppSwitchRow(
leading: const AppIconBadge(
icon: CupertinoIcons.lock_fill,
color: AppColors.systemGray,
),
title: 'Face ID',
value: false,
onChanged: (_) {},
),
],
),
);
},
),
],
);
}
@@ -0,0 +1,73 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent dialogsComponent() {
return WidgetbookComponent(
name: 'AppAlert',
useCases: [
WidgetbookUseCase(
name: 'Alert',
builder: (context) {
final message = knobText(
context,
label: 'Message',
initialValue: 'Операция будет удалена без возможности восстановления.',
);
return IosPreview.centered(
child: AppAlert(
title: knobText(context, label: 'Title', initialValue: 'Удалить трату?'),
message: message.isEmpty ? null : message,
confirmLabel: knobText(context, label: 'Confirm', initialValue: 'Удалить'),
cancelLabel: knobBool(context, label: 'Cancel button', initialValue: true)
? 'Отмена'
: null,
destructive: knobBool(context, label: 'Destructive', initialValue: true),
),
);
},
),
WidgetbookUseCase(
name: 'Triggers',
builder: (context) {
return IosPreview(
stretch: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AppButton(
label: 'Показать alert',
onPressed: () => showAppAlert(
context: context,
title: 'Удалить трату?',
message: 'Действие необратимо.',
confirmLabel: 'Удалить',
cancelLabel: 'Отмена',
destructive: true,
),
),
const SizedBox(height: AppSpacing.s3),
AppButton(
label: 'Показать action sheet',
style: AppButtonStyle.gray,
onPressed: () => showAppActionSheet(
context: context,
title: 'Операция',
actions: const [
AppActionSheetAction(label: 'Редактировать', isDefault: true),
AppActionSheetAction(label: 'Дублировать'),
AppActionSheetAction(label: 'Удалить', destructive: true),
],
),
),
],
),
);
},
),
],
);
}
@@ -0,0 +1,83 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent navBarComponent() {
return WidgetbookComponent(
name: 'AppNavBar',
useCases: [
WidgetbookUseCase(
name: 'Playground',
builder: (context) {
final subtitle = knobText(context, label: 'Subtitle', initialValue: 'До 25 сентября');
return CupertinoPageScaffold(
backgroundColor: AppColors.of(context, AppColors.groupedBackground),
navigationBar: AppNavBar(
title: knobText(context, label: 'Title', initialValue: 'Бюджет'),
subtitle: subtitle.isEmpty ? null : subtitle,
previousPageTitle:
knobBool(context, label: 'Back button', initialValue: true) ? 'Назад' : null,
transparent: knobBool(context, label: 'Transparent'),
trailing: knobBool(context, label: 'Trailing action', initialValue: true)
? CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
onPressed: () {},
child: const AppIcon(
CupertinoIcons.ellipsis_circle,
color: AppColors.accent,
),
)
: null,
),
child: const SafeArea(
child: Padding(
padding: EdgeInsets.all(AppSpacing.s4),
child: AppText.subhead('Контент под навигационной панелью'),
),
),
);
},
),
WidgetbookUseCase(
name: 'Large title',
builder: (context) {
return CupertinoPageScaffold(
backgroundColor: AppColors.of(context, AppColors.groupedBackground),
child: CustomScrollView(
slivers: [
AppLargeNavBar(
title: knobText(context, label: 'Title', initialValue: 'Журнал'),
trailing: CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
onPressed: () {},
child: const AppIcon(
CupertinoIcons.add_circled,
color: AppColors.accent,
),
),
),
SliverToBoxAdapter(
child: AppListSection(
header: 'Сегодня',
separatorIndent: 60,
children: const [
AppListTile(
leading: AppIconBadge(icon: CupertinoIcons.cart_fill),
title: 'Продукты',
value: '1 840 ₽',
),
],
),
),
],
),
);
},
),
],
);
}
@@ -0,0 +1,93 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:please_pay_me_widgetbook/support/preview.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent segmentedControlComponent() {
return WidgetbookComponent(
name: 'AppSegmentedControl',
useCases: [
WidgetbookUseCase(
name: 'Playground',
builder: (context) {
final labels = knobText(
context,
label: 'Labels (comma separated)',
initialValue: 'Все,Расходы,Доходы',
).split(',').where((e) => e.trim().isNotEmpty).toList();
final safeLabels = labels.isEmpty ? ['Все'] : labels;
return IosPreview(
stretch: true,
child: _StatefulSegments(
labels: safeLabels,
initialIndex: knobInt(
context,
label: 'Initial index',
max: safeLabels.length - 1,
),
),
);
},
),
WidgetbookUseCase(
name: 'In screen header',
builder: (context) {
return CupertinoPageScaffold(
backgroundColor: AppColors.of(context, AppColors.groupedBackground),
navigationBar: const AppNavBar(title: 'Журнал'),
child: SafeArea(
child: Column(
children: [
const SizedBox(height: AppSpacing.s4),
const _StatefulSegments(labels: ['Неделя', 'Месяц', 'Год']),
const SizedBox(height: AppSpacing.s5),
AppListSection(
children: const [
AppListTile(title: 'Итого', value: '45 580 ₽'),
],
),
],
),
),
);
},
),
],
);
}
class _StatefulSegments extends StatefulWidget {
const _StatefulSegments({required this.labels, this.initialIndex = 0});
final List<String> labels;
final int initialIndex;
@override
State<_StatefulSegments> createState() => _StatefulSegmentsState();
}
class _StatefulSegmentsState extends State<_StatefulSegments> {
late int _index = widget.initialIndex;
@override
void didUpdateWidget(_StatefulSegments oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialIndex != widget.initialIndex ||
_index >= widget.labels.length) {
_index = widget.initialIndex.clamp(0, widget.labels.length - 1);
}
}
@override
Widget build(BuildContext context) {
return AppSegmentedControl(
labels: widget.labels,
index: _index,
onChanged: (i) => setState(() => _index = i),
);
}
}
@@ -0,0 +1,53 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:please_pay_me_widgetbook/knobs/common_knobs.dart';
import 'package:widgetbook/widgetbook.dart';
const _items = [
AppTabItem(icon: CupertinoIcons.list_bullet, label: 'Журнал'),
AppTabItem(icon: CupertinoIcons.chart_pie, label: 'Бюджет'),
AppTabItem(
icon: CupertinoIcons.person,
activeIcon: CupertinoIcons.person_fill,
label: 'Профиль',
),
];
WidgetbookComponent tabBarComponent() {
return WidgetbookComponent(
name: 'AppTabBar',
useCases: [
WidgetbookUseCase(
name: 'Playground',
builder: (context) {
final index = knobInt(context, label: 'Current index', min: 0, max: 2);
return ColoredBox(
color: AppColors.of(context, AppColors.groupedBackground),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AppTabBar(items: _items, currentIndex: index, onTap: (_) {}),
],
),
);
},
),
WidgetbookUseCase(
name: 'In scaffold',
builder: (context) {
return CupertinoTabScaffold(
tabBar: AppTabBar(items: _items, currentIndex: 0, onTap: (_) {}),
tabBuilder: (context, index) => CupertinoTabView(
builder: (context) => CupertinoPageScaffold(
backgroundColor: AppColors.of(context, AppColors.groupedBackground),
navigationBar: AppNavBar(title: _items[index].label),
child: Center(child: AppText.body(_items[index].label)),
),
),
);
},
),
],
);
}
@@ -0,0 +1,130 @@
import 'package:please_pay_me/app/home_tabs.dart';
import 'package:please_pay_me/features/auth/login_screen.dart';
import 'package:please_pay_me/features/budgets/budgets_screen.dart';
import 'package:please_pay_me/features/journal/journal_screen.dart';
import 'package:please_pay_me/features/overview/overview_screen.dart';
import 'package:please_pay_me/features/profile/profile_screen.dart';
import 'package:please_pay_me/features/splash/splash_screen.dart';
import 'package:please_pay_me/features/work/work_screen.dart';
import 'package:please_pay_me_widgetbook/support/demo_scope.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent overviewScreenComponent() {
return WidgetbookComponent(
name: 'OverviewScreen',
useCases: [
WidgetbookUseCase(
name: 'С активным бюджетом',
builder: (context) => const DemoScope(child: OverviewScreen()),
),
WidgetbookUseCase(
name: 'Без бюджетов',
builder: (context) => const DemoScope(seeded: false, child: OverviewScreen()),
),
],
);
}
WidgetbookComponent journalScreenComponent() {
return WidgetbookComponent(
name: 'JournalScreen',
useCases: [
WidgetbookUseCase(
name: 'С операциями',
builder: (context) => const DemoScope(child: JournalScreen()),
),
WidgetbookUseCase(
name: 'Пустой',
builder: (context) => const DemoScope(seeded: false, child: JournalScreen()),
),
],
);
}
WidgetbookComponent budgetsScreenComponent() {
return WidgetbookComponent(
name: 'BudgetsScreen',
useCases: [
WidgetbookUseCase(
name: 'Активные и завершённые',
builder: (context) => const DemoScope(child: BudgetsScreen()),
),
WidgetbookUseCase(
name: 'Пустой',
builder: (context) => const DemoScope(seeded: false, child: BudgetsScreen()),
),
],
);
}
WidgetbookComponent workScreenComponent() {
return WidgetbookComponent(
name: 'WorkScreen',
useCases: [
WidgetbookUseCase(
name: 'С работой',
builder: (context) => const DemoScope(child: WorkScreen()),
),
WidgetbookUseCase(
name: 'Пустой',
builder: (context) => const DemoScope(seeded: false, child: WorkScreen()),
),
],
);
}
WidgetbookComponent profileScreenComponent() {
return WidgetbookComponent(
name: 'ProfileScreen',
useCases: [
WidgetbookUseCase(
name: 'Демо-сессия',
builder: (context) => const DemoScope(child: ProfileScreen()),
),
],
);
}
WidgetbookComponent splashScreenComponent() {
return WidgetbookComponent(
name: 'SplashScreen',
useCases: [
WidgetbookUseCase(
name: 'Запуск',
builder: (context) => const SplashScreen(),
),
],
);
}
WidgetbookComponent loginScreenComponent() {
return WidgetbookComponent(
name: 'LoginScreen',
useCases: [
WidgetbookUseCase(
name: 'Вход через Telegram',
builder: (context) => DemoScope(
// Desktop has no WebView, so the catalog stubs the launcher to show
// the mobile layout.
child: LoginScreen(launchTelegramLogin: (_, __) async => null),
),
),
WidgetbookUseCase(
name: 'Вход по токену',
builder: (context) => const DemoScope(child: LoginScreen()),
),
],
);
}
WidgetbookComponent homeTabsComponent() {
return WidgetbookComponent(
name: 'HomeTabs',
useCases: [
WidgetbookUseCase(
name: 'Полное приложение',
builder: (context) => const DemoScope(child: HomeTabs()),
),
],
);
}
@@ -0,0 +1,64 @@
import 'package:please_pay_me/data/demo/demo_backend.dart';
import 'package:please_pay_me/features/budgets/budget_form_sheet.dart';
import 'package:please_pay_me/features/expenses/expense_form_sheet.dart';
import 'package:please_pay_me/features/work/job_form_sheet.dart';
import 'package:please_pay_me_widgetbook/support/demo_scope.dart';
import 'package:widgetbook/widgetbook.dart';
WidgetbookComponent sheetsComponent() {
return WidgetbookComponent(
name: 'Формы',
useCases: [
WidgetbookUseCase(
name: 'Новая трата',
builder: (context) => DemoScope(
child: ExpenseFormSheet(
budgetName: 'До аванса',
remainingToday: 1420,
onSubmit: ({required amount, note, required spentAt}) async => null,
),
),
),
WidgetbookUseCase(
name: 'Новый бюджет',
builder: (context) => DemoScope(
child: BudgetFormSheet(
onSubmit: ({
required name,
required totalAmount,
required startDate,
required endDate,
required resetExpenses,
}) async =>
null,
),
),
),
WidgetbookUseCase(
name: 'Новая работа',
builder: (context) => DemoScope(
child: JobFormSheet(
onSubmit: ({
required name,
required salaryAmount,
required payDays,
required firstPayPercent,
required weekendPolicy,
}) async =>
null,
),
),
),
WidgetbookUseCase(
name: 'Ошибка сохранения',
builder: (context) => DemoScope(
child: ExpenseFormSheet(
budgetName: DemoBackend.user.displayName,
onSubmit: ({required amount, note, required spentAt}) async =>
'Бюджет уже завершён',
),
),
),
],
);
}