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:
Hamza-Ayed
2026-08-04 21:42:19 +03:00
co-authored by Claude Opus 5
parent 0de8573d27
commit c52338f3bb
117 changed files with 6061 additions and 374 deletions
@@ -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,
);