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
+53
View File
@@ -0,0 +1,53 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:please_pay_me/theme/tokens.dart';
/// Inter is the closest cross-platform stand-in for SF Pro, so the catalog
/// looks the same on Windows/web as it does on device.
TextStyle _sf(TextStyle style, {Color? color}) {
return GoogleFonts.inter(textStyle: style, color: color);
}
CupertinoTextThemeData _textTheme() {
return CupertinoTextThemeData(
primaryColor: AppColors.accent,
textStyle: _sf(AppTypography.body, color: AppColors.label),
actionTextStyle: _sf(AppTypography.body, color: AppColors.accent),
tabLabelTextStyle: _sf(AppTypography.caption2, color: AppColors.secondaryLabel),
navTitleTextStyle: _sf(AppTypography.headline, color: AppColors.label),
navLargeTitleTextStyle: _sf(AppTypography.largeTitle, color: AppColors.label),
navActionTextStyle: _sf(AppTypography.body, color: AppColors.accent),
);
}
CupertinoThemeData buildLightTheme() => _buildTheme(Brightness.light);
CupertinoThemeData buildDarkTheme() => _buildTheme(Brightness.dark);
CupertinoThemeData _buildTheme(Brightness brightness) {
return CupertinoThemeData(
brightness: brightness,
primaryColor: AppColors.accent,
primaryContrastingColor: const Color(0xFFFFFFFF),
scaffoldBackgroundColor: AppColors.groupedBackground,
barBackgroundColor: AppColors.barBackground,
applyThemeToAll: true,
textTheme: _textTheme(),
);
}
/// Android system bars. Dark uses transparent black so the 3-button / gesture
/// bar does not get a gray contrast scrim over the tab bar.
SystemUiOverlayStyle systemUiOverlayFor(Brightness brightness) {
final dark = brightness == Brightness.dark;
return SystemUiOverlayStyle(
statusBarColor: const Color(0x00000000),
statusBarBrightness: brightness,
statusBarIconBrightness: dark ? Brightness.light : Brightness.dark,
systemNavigationBarColor: dark ? const Color(0x00000000) : const Color(0x00FFFFFF),
systemNavigationBarDividerColor: const Color(0x00000000),
systemNavigationBarIconBrightness: dark ? Brightness.light : Brightness.dark,
systemNavigationBarContrastEnforced: false,
);
}
+3
View File
@@ -0,0 +1,3 @@
export 'app_theme.dart';
export 'theme_controller.dart';
export 'tokens.dart';
+81
View File
@@ -0,0 +1,81 @@
import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart';
enum ThemePreference {
system,
light,
dark;
static ThemePreference parse(String? raw) {
return switch (raw) {
'light' => ThemePreference.light,
'dark' => ThemePreference.dark,
_ => ThemePreference.system,
};
}
Brightness resolve(Brightness platform) => switch (this) {
ThemePreference.system => platform,
ThemePreference.light => Brightness.light,
ThemePreference.dark => Brightness.dark,
};
}
abstract interface class ThemePreferenceStore {
Future<String?> read();
Future<void> write(String value);
}
class PrefsThemeStore implements ThemePreferenceStore {
const PrefsThemeStore();
static const key = 'ppm_theme_preference';
@override
Future<String?> read() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(key);
}
@override
Future<void> write(String value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
}
}
class MemoryThemeStore implements ThemePreferenceStore {
MemoryThemeStore([this.value]);
String? value;
@override
Future<String?> read() async => value;
@override
Future<void> write(String next) async => value = next;
}
/// Survives logout: appearance is a device preference, not a session one.
class ThemeController extends ChangeNotifier {
ThemeController({ThemePreferenceStore? store}) : _store = store ?? const PrefsThemeStore();
final ThemePreferenceStore _store;
ThemePreference _preference = ThemePreference.system;
ThemePreference get preference => _preference;
Brightness resolve(Brightness platform) => _preference.resolve(platform);
Future<void> restore() async {
_preference = ThemePreference.parse(await _store.read());
notifyListeners();
}
Future<void> setPreference(ThemePreference value) async {
if (_preference == value) return;
_preference = value;
notifyListeners();
await _store.write(value.name);
}
}
+138
View File
@@ -0,0 +1,138 @@
/// Design tokens for the iOS-styled kit.
///
/// Semantic colors follow Apple HIG (label / separator / grouped background)
/// and are declared as [CupertinoDynamicColor] so widgets resolve light & dark
/// automatically. Brand tint stays green to match the web cabinet.
library;
import 'package:flutter/cupertino.dart';
abstract final class AppColors {
/// Brand tint — replaces `systemBlue` across the kit.
static const accent = CupertinoDynamicColor.withBrightness(
color: Color(0xFF12885A),
darkColor: Color(0xFF3CD68C),
);
static const accentSoft = CupertinoDynamicColor.withBrightness(
color: Color(0xFFE4F4EC),
darkColor: Color(0xFF14301F),
);
// —— iOS system palette ——
static const systemRed = CupertinoDynamicColor.withBrightness(
color: Color(0xFFFF3B30),
darkColor: Color(0xFFFF453A),
);
static const systemOrange = CupertinoDynamicColor.withBrightness(
color: Color(0xFFFF9500),
darkColor: Color(0xFFFF9F0A),
);
static const systemGreen = CupertinoDynamicColor.withBrightness(
color: Color(0xFF34C759),
darkColor: Color(0xFF30D158),
);
static const systemGray = CupertinoDynamicColor.withBrightness(
color: Color(0xFF8E8E93),
darkColor: Color(0xFF8E8E93),
);
static const systemGray3 = CupertinoDynamicColor.withBrightness(
color: Color(0xFFC7C7CC),
darkColor: Color(0xFF48484A),
);
static const systemGray5 = CupertinoDynamicColor.withBrightness(
color: Color(0xFFE5E5EA),
darkColor: Color(0xFF2C2C2E),
);
static const systemGray6 = CupertinoDynamicColor.withBrightness(
color: Color(0xFFF2F2F7),
darkColor: Color(0xFF1C1C1E),
);
// —— Text hierarchy ——
static const label = CupertinoDynamicColor.withBrightness(
color: Color(0xFF000000),
darkColor: Color(0xFFFFFFFF),
);
static const secondaryLabel = CupertinoDynamicColor.withBrightness(
color: Color(0x993C3C43),
darkColor: Color(0x99EBEBF5),
);
static const tertiaryLabel = CupertinoDynamicColor.withBrightness(
color: Color(0x4D3C3C43),
darkColor: Color(0x4DEBEBF5),
);
// —— Separators & surfaces ——
static const separator = CupertinoDynamicColor.withBrightness(
color: Color(0x4A3C3C43),
darkColor: Color(0xA6545458),
);
static const opaqueSeparator = CupertinoDynamicColor.withBrightness(
color: Color(0xFFC6C6C8),
darkColor: Color(0xFF38383A),
);
static const groupedBackground = CupertinoDynamicColor.withBrightness(
color: Color(0xFFF2F2F7),
darkColor: Color(0xFF000000),
);
static const groupedSurface = CupertinoDynamicColor.withBrightness(
color: Color(0xFFFFFFFF),
darkColor: Color(0xFF1C1C1E),
);
static const barBackground = CupertinoDynamicColor.withBrightness(
color: Color(0xF0F9F9F9),
darkColor: Color(0xF01D1D1D),
);
static Color of(BuildContext context, Color color) =>
CupertinoDynamicColor.resolve(color, context);
}
/// 4pt grid; iOS content inset is 16.
abstract final class AppSpacing {
static const double s1 = 4;
static const double s2 = 8;
static const double s3 = 12;
static const double s4 = 16;
static const double s5 = 20;
static const double s6 = 28;
static const double s7 = 40;
static const double s8 = 56;
/// Standard leading/trailing inset for grouped content.
static const double gutter = 16;
}
abstract final class AppRadii {
static const double sm = 6;
static const double md = 10;
/// Inset-grouped cards & large buttons.
static const double lg = 12;
static const double xl = 16;
static const double capsule = 999;
}
abstract final class AppSizes {
static const double buttonLarge = 50;
static const double buttonMedium = 44;
static const double buttonSmall = 34;
static const double rowMinHeight = 44;
static const double hairline = 0.5;
}
/// SF Pro text scale (Apple HIG).
abstract final class AppTypography {
static const largeTitle = TextStyle(fontSize: 34, height: 1.2, fontWeight: FontWeight.w700, letterSpacing: 0.37);
static const title1 = TextStyle(fontSize: 28, height: 1.2, fontWeight: FontWeight.w700, letterSpacing: 0.36);
static const title2 = TextStyle(fontSize: 22, height: 1.25, fontWeight: FontWeight.w700, letterSpacing: 0.35);
static const title3 = TextStyle(fontSize: 20, height: 1.25, fontWeight: FontWeight.w600, letterSpacing: 0.38);
static const headline = TextStyle(fontSize: 17, height: 1.3, fontWeight: FontWeight.w600, letterSpacing: -0.41);
static const body = TextStyle(fontSize: 17, height: 1.3, fontWeight: FontWeight.w400, letterSpacing: -0.41);
static const callout = TextStyle(fontSize: 16, height: 1.3, fontWeight: FontWeight.w400, letterSpacing: -0.32);
static const subhead = TextStyle(fontSize: 15, height: 1.3, fontWeight: FontWeight.w400, letterSpacing: -0.24);
static const footnote = TextStyle(fontSize: 13, height: 1.3, fontWeight: FontWeight.w400, letterSpacing: -0.08);
static const caption1 = TextStyle(fontSize: 12, height: 1.3, fontWeight: FontWeight.w400);
static const caption2 = TextStyle(fontSize: 11, height: 1.3, fontWeight: FontWeight.w400, letterSpacing: 0.07);
}