feat(proj): init
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'app_theme.dart';
|
||||
export 'theme_controller.dart';
|
||||
export 'tokens.dart';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user