feat(apps): نظام التصميم والمصادقة الكاملة — المرحلتان 2 و3
## تصحيح بنيوي أولاً
بنية المرحلة 1 كانت تخالف docs/23 §1 المُلزِم. أُعيدت للشجرة المفروضة
حرفياً: app.dart في الجذر · core/{config,build_config,di,router}.dart ·
core/{design,ui,l10n,api,storage,session}/ · features/<f>/{cubit,data,view}/
## المرحلة 2 — نظام التصميم (docs/26)
- core/design: tokens (مسافات/زوايا/حركة/أحجام — الشاشة لا تخترع رقماً) ·
typography (IBM Plex Sans Arabic + Inter محليّان، بأرقام tabular للأسعار
والعدّادات) · TripzColors كـThemeExtension للأدوار الدلالية ·
buildTheme(brightness, locale) بمدخلين لا ثالث لهما
- core/ui: 11 مكوّناً. TripzScaffold يحوّل status الـCubit وحده إلى
skeleton/خطأ/فراغ/محتوى — فلا تكتب أي شاشة if (loading)
- core/l10n: ARB عربي/إنجليزي + gen_l10n. لا نص مرئي داخل widget
- SettingsCubit: المظهر واللغة. الاتجاه يتبع اللغة آلياً بلا Directionality
مفروضة، وسقف تكبير النص 1.3
## المرحلة 3 — المصادقة
- طبقة الشبكة: تجديد استباقي بطلقة واحدة (عمر التوكن 15 دقيقة) ·
x-app-role · x-device-id من device_info_plus — يُرسَل الآن كي يُفعَّل علم
الخادم لاحقاً بلا تعديل التطبيق
- SessionCubit في core/session خلف واجهة SessionSource: الجلسة حالة على
مستوى التطبيق لا ميزة، و core لا يستورد من features
- LoginCubit بخطواته الخمس، ProfileCubit، وست شاشات:
الشروط ← إذن الموقع ← الهاتف ← الرمز ← إكمال الملف ← هيكل الرئيسية
من سيرو نُقل السلوك لا الكود: بوابتان قبل أي حقل إدخال · إعادة فحص الإذن عند
العودة من إعدادات النظام · تحقّق الهاتف الثلاثي. وأُضيف ما ينقصه: عدّاد إعادة
إرسال الرمز — بدونه تُحظر ثلاث ضغطات المستخدمَ خمس دقائق (docs/38 §2).
قراران موثّقان في docs/37: لا packages/tripz_ui (الوثيقتان المُلزِمتان تفرضان
core/design و core/ui داخل التطبيق)، والـCubit يُصدر رمز فشل لا نصّاً
(يجمع بين docs/23 §3 و docs/26 §4).
flutter analyze نظيف في التطبيقين · 48 ملف و3,140 سطر لكل تطبيق · الفرق
بينهما أربعة ملفات فقط: build_config · config · ملفّا ARB.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0de8573d27
commit
c52338f3bb
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../build_config.dart';
|
||||
import 'tokens.dart';
|
||||
import 'tripz_colors.dart';
|
||||
import 'typography.dart';
|
||||
|
||||
/// دالة بناء الثيم الوحيدة (docs/26 §4): مدخلان لا ثالث لهما.
|
||||
///
|
||||
/// الاتجاه (RTL/LTR) **لا يُفرض هنا** — `MaterialApp` يشتقّه من الـlocale
|
||||
/// وحده. فرض `Directionality.rtl` يكسر الإنجليزية (docs/26 §4).
|
||||
ThemeData buildTheme(Brightness brightness, Locale locale) {
|
||||
final scheme = ColorScheme.fromSeed(
|
||||
seedColor: BuildConfig.seedColor,
|
||||
brightness: brightness,
|
||||
);
|
||||
final isDark = brightness == Brightness.dark;
|
||||
final texts = buildTextTheme(brightness, locale);
|
||||
final tripz = isDark ? TripzColors.dark : TripzColors.light;
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: scheme,
|
||||
brightness: brightness,
|
||||
fontFamily: AppFonts.familyFor(locale),
|
||||
textTheme: texts,
|
||||
scaffoldBackgroundColor: scheme.surface,
|
||||
extensions: [tripz],
|
||||
|
||||
// AppBar مسطّح بلا ظل (docs/26 §6).
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: scheme.surface,
|
||||
foregroundColor: scheme.onSurface,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
centerTitle: false,
|
||||
titleTextStyle: texts.titleLarge?.copyWith(color: scheme.onSurface),
|
||||
),
|
||||
|
||||
cardTheme: CardThemeData(
|
||||
color: tripz.surfaceRaised,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: Radii.card_,
|
||||
side: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
),
|
||||
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: scheme.surface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
showDragHandle: true,
|
||||
shape: const RoundedRectangleBorder(borderRadius: Radii.sheetTop),
|
||||
),
|
||||
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: isDark ? scheme.surfaceContainerHigh : scheme.surface,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: Space.md,
|
||||
vertical: Space.sm,
|
||||
),
|
||||
border: _fieldBorder(scheme.outlineVariant),
|
||||
enabledBorder: _fieldBorder(scheme.outlineVariant),
|
||||
focusedBorder: _fieldBorder(scheme.primary, width: 1.5),
|
||||
errorBorder: _fieldBorder(scheme.error),
|
||||
focusedErrorBorder: _fieldBorder(scheme.error, width: 1.5),
|
||||
errorStyle: texts.bodySmall?.copyWith(color: scheme.error),
|
||||
),
|
||||
|
||||
dividerTheme: DividerThemeData(
|
||||
color: scheme.outlineVariant,
|
||||
space: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: scheme.inverseSurface,
|
||||
contentTextStyle:
|
||||
texts.bodyMedium?.copyWith(color: scheme.onInverseSurface),
|
||||
shape: const RoundedRectangleBorder(borderRadius: Radii.card_),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
OutlineInputBorder _fieldBorder(Color color, {double width = 1}) {
|
||||
return OutlineInputBorder(
|
||||
borderRadius: Radii.field_,
|
||||
borderSide: BorderSide(color: color, width: width),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// الرموز الثابتة (docs/26 §3). **الشاشة لا تخترع رقماً** — لا مسافة ولا زاوية
|
||||
/// ولا مدّة خارج هذا الملف.
|
||||
class Space {
|
||||
const Space._();
|
||||
|
||||
/// سلّم مضاعفات الأربعة.
|
||||
static const double xxs = 4;
|
||||
static const double xs = 8;
|
||||
static const double sm = 12;
|
||||
|
||||
/// حشوة الصفحة الافتراضية.
|
||||
static const double md = 16;
|
||||
static const double lg = 24;
|
||||
static const double xl = 32;
|
||||
static const double xxl = 48;
|
||||
|
||||
static const EdgeInsets page = EdgeInsets.all(md);
|
||||
static const EdgeInsets pageH = EdgeInsets.symmetric(horizontal: md);
|
||||
}
|
||||
|
||||
class Radii {
|
||||
const Radii._();
|
||||
|
||||
/// حقول وشارات.
|
||||
static const double field = 8;
|
||||
|
||||
/// أزرار وبطاقات.
|
||||
static const double card = 12;
|
||||
|
||||
/// الأوراق السفلية.
|
||||
static const double sheet = 24;
|
||||
|
||||
static const BorderRadius field_ = BorderRadius.all(Radius.circular(field));
|
||||
static const BorderRadius card_ = BorderRadius.all(Radius.circular(card));
|
||||
static const BorderRadius sheetTop = BorderRadius.vertical(
|
||||
top: Radius.circular(sheet),
|
||||
);
|
||||
}
|
||||
|
||||
class Motion {
|
||||
const Motion._();
|
||||
|
||||
/// استجابة لمسة.
|
||||
static const Duration tap = Duration(milliseconds: 120);
|
||||
|
||||
/// انتقال ضمن الشاشة.
|
||||
static const Duration inScreen = Duration(milliseconds: 240);
|
||||
|
||||
/// ورقة سفلية أو صفحة.
|
||||
static const Duration page = Duration(milliseconds: 400);
|
||||
|
||||
static const Curve curve = Curves.easeOutCubic;
|
||||
}
|
||||
|
||||
class Sizes {
|
||||
const Sizes._();
|
||||
|
||||
static const double iconSm = 20;
|
||||
static const double icon = 24;
|
||||
static const double iconLg = 32;
|
||||
|
||||
/// ارتفاع زر الـCTA وحقل الإدخال.
|
||||
static const double control = 52;
|
||||
|
||||
/// أدنى هدف لمس مقبول.
|
||||
static const double touchTarget = 48;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// الأدوار الدلالية التي لا يوفّرها `ColorScheme` (docs/26 §2).
|
||||
///
|
||||
/// الشاشة تقول `context.tripzColors.success` — **ولا تقرّر درجة اللون بنفسها،
|
||||
/// ولا تكتب hex**. تُشتقّ كلها من السطوع لا من لون المستأجر، فتبقى ثابتة عبر
|
||||
/// كل المستأجرين كما يفرض عقد §0.
|
||||
@immutable
|
||||
class TripzColors extends ThemeExtension<TripzColors> {
|
||||
const TripzColors({
|
||||
required this.success,
|
||||
required this.onSuccess,
|
||||
required this.warning,
|
||||
required this.onWarning,
|
||||
required this.danger,
|
||||
required this.onDanger,
|
||||
required this.info,
|
||||
required this.surfaceRaised,
|
||||
required this.mapRoute,
|
||||
required this.mapOrigin,
|
||||
required this.mapDestination,
|
||||
required this.skeleton,
|
||||
});
|
||||
|
||||
final Color success;
|
||||
final Color onSuccess;
|
||||
final Color warning;
|
||||
final Color onWarning;
|
||||
final Color danger;
|
||||
final Color onDanger;
|
||||
final Color info;
|
||||
|
||||
/// السطح المرتفع — في الليلي ارتفاعٌ **بالسطوع لا بالظل** (docs/26 §2).
|
||||
final Color surfaceRaised;
|
||||
|
||||
final Color mapRoute;
|
||||
final Color mapOrigin;
|
||||
final Color mapDestination;
|
||||
|
||||
/// لون هيكل التحميل (skeleton) — لا spinner فارغ (docs/26 §8.4).
|
||||
final Color skeleton;
|
||||
|
||||
static const light = TripzColors(
|
||||
success: Color(0xFF1B8A5A),
|
||||
onSuccess: Color(0xFFFFFFFF),
|
||||
warning: Color(0xFFB4690E),
|
||||
onWarning: Color(0xFFFFFFFF),
|
||||
danger: Color(0xFFC5303B),
|
||||
onDanger: Color(0xFFFFFFFF),
|
||||
info: Color(0xFF2563A8),
|
||||
surfaceRaised: Color(0xFFFFFFFF),
|
||||
mapRoute: Color(0xFF1F6FEB),
|
||||
mapOrigin: Color(0xFF1B8A5A),
|
||||
mapDestination: Color(0xFFC5303B),
|
||||
skeleton: Color(0xFFE6E8EC),
|
||||
);
|
||||
|
||||
static const dark = TripzColors(
|
||||
success: Color(0xFF4ECB8D),
|
||||
onSuccess: Color(0xFF00281A),
|
||||
warning: Color(0xFFE9A63B),
|
||||
onWarning: Color(0xFF2A1A00),
|
||||
danger: Color(0xFFF2707A),
|
||||
onDanger: Color(0xFF33000A),
|
||||
info: Color(0xFF7FB2F0),
|
||||
// ليس أسود صرفاً — رمادي داكن متدرّج.
|
||||
surfaceRaised: Color(0xFF20232A),
|
||||
mapRoute: Color(0xFF5B9BFF),
|
||||
mapOrigin: Color(0xFF4ECB8D),
|
||||
mapDestination: Color(0xFFF2707A),
|
||||
skeleton: Color(0xFF2C3038),
|
||||
);
|
||||
|
||||
@override
|
||||
TripzColors copyWith({
|
||||
Color? success,
|
||||
Color? onSuccess,
|
||||
Color? warning,
|
||||
Color? onWarning,
|
||||
Color? danger,
|
||||
Color? onDanger,
|
||||
Color? info,
|
||||
Color? surfaceRaised,
|
||||
Color? mapRoute,
|
||||
Color? mapOrigin,
|
||||
Color? mapDestination,
|
||||
Color? skeleton,
|
||||
}) {
|
||||
return TripzColors(
|
||||
success: success ?? this.success,
|
||||
onSuccess: onSuccess ?? this.onSuccess,
|
||||
warning: warning ?? this.warning,
|
||||
onWarning: onWarning ?? this.onWarning,
|
||||
danger: danger ?? this.danger,
|
||||
onDanger: onDanger ?? this.onDanger,
|
||||
info: info ?? this.info,
|
||||
surfaceRaised: surfaceRaised ?? this.surfaceRaised,
|
||||
mapRoute: mapRoute ?? this.mapRoute,
|
||||
mapOrigin: mapOrigin ?? this.mapOrigin,
|
||||
mapDestination: mapDestination ?? this.mapDestination,
|
||||
skeleton: skeleton ?? this.skeleton,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TripzColors lerp(ThemeExtension<TripzColors>? other, double t) {
|
||||
if (other is! TripzColors) return this;
|
||||
return TripzColors(
|
||||
success: Color.lerp(success, other.success, t)!,
|
||||
onSuccess: Color.lerp(onSuccess, other.onSuccess, t)!,
|
||||
warning: Color.lerp(warning, other.warning, t)!,
|
||||
onWarning: Color.lerp(onWarning, other.onWarning, t)!,
|
||||
danger: Color.lerp(danger, other.danger, t)!,
|
||||
onDanger: Color.lerp(onDanger, other.onDanger, t)!,
|
||||
info: Color.lerp(info, other.info, t)!,
|
||||
surfaceRaised: Color.lerp(surfaceRaised, other.surfaceRaised, t)!,
|
||||
mapRoute: Color.lerp(mapRoute, other.mapRoute, t)!,
|
||||
mapOrigin: Color.lerp(mapOrigin, other.mapOrigin, t)!,
|
||||
mapDestination: Color.lerp(mapDestination, other.mapDestination, t)!,
|
||||
skeleton: Color.lerp(skeleton, other.skeleton, t)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension TripzColorsX on BuildContext {
|
||||
TripzColors get tripzColors => Theme.of(this).extension<TripzColors>()!;
|
||||
ColorScheme get colors => Theme.of(this).colorScheme;
|
||||
TextTheme get texts => Theme.of(this).textTheme;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// الخطوط (docs/26 §1) — ملفات محلية في `assets/fonts/`. حزمة `google_fonts`
|
||||
/// ممنوعة: الشكل يجب أن يكون حتمياً من أول إقلاع بلا شبكة.
|
||||
class AppFonts {
|
||||
const AppFonts._();
|
||||
|
||||
static const arabic = 'IBMPlexSansArabic';
|
||||
static const latin = 'Inter';
|
||||
|
||||
/// الخط يتبع اللغة، والآخر احتياط — كي لا تنكسر كلمة عربية داخل جملة
|
||||
/// إنجليزية ولا العكس.
|
||||
static String familyFor(Locale locale) =>
|
||||
locale.languageCode == 'ar' ? arabic : latin;
|
||||
|
||||
static List<String> fallbackFor(Locale locale) =>
|
||||
locale.languageCode == 'ar' ? const [latin] : const [arabic];
|
||||
}
|
||||
|
||||
/// أرقام tabular: تمنع «رقص» الأعمدة حين تتغيّر القيمة — للأسعار والعدّادات
|
||||
/// والمسافات الحيّة (docs/26 §1). يُغني عن خط `digit` المتقاعد.
|
||||
const tabularFigures = [FontFeature.tabularFigures()];
|
||||
|
||||
TextTheme buildTextTheme(Brightness brightness, Locale locale) {
|
||||
final base = brightness == Brightness.dark
|
||||
? Typography.material2021().white
|
||||
: Typography.material2021().black;
|
||||
|
||||
final family = AppFonts.familyFor(locale);
|
||||
final fallback = AppFonts.fallbackFor(locale);
|
||||
|
||||
return base
|
||||
.apply(fontFamily: family, fontFamilyFallback: fallback)
|
||||
.copyWith(
|
||||
displaySmall: base.displaySmall?.copyWith(
|
||||
fontFamily: family,
|
||||
fontFamilyFallback: fallback,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
headlineMedium: base.headlineMedium?.copyWith(
|
||||
fontFamily: family,
|
||||
fontFamilyFallback: fallback,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
headlineSmall: base.headlineSmall?.copyWith(
|
||||
fontFamily: family,
|
||||
fontFamilyFallback: fallback,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
titleLarge: base.titleLarge?.copyWith(
|
||||
fontFamily: family,
|
||||
fontFamilyFallback: fallback,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
titleMedium: base.titleMedium?.copyWith(
|
||||
fontFamily: family,
|
||||
fontFamilyFallback: fallback,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
labelLarge: base.labelLarge?.copyWith(
|
||||
fontFamily: family,
|
||||
fontFamilyFallback: fallback,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// نمط الأرقام الحيّة — يُستعمل للأجرة والرصيد والعدّاد التنازلي.
|
||||
/// دائماً Inter بأرقام tabular، مهما كانت لغة الواجهة (docs/26 §5: الأرقام
|
||||
/// لاتينية في اللغتين).
|
||||
TextStyle numericStyle(TextStyle? base) => (base ?? const TextStyle()).copyWith(
|
||||
fontFamily: AppFonts.latin,
|
||||
fontFeatures: tabularFigures,
|
||||
);
|
||||
Reference in New Issue
Block a user