الجوال والشاشات

هوية أوكتا داخل التطبيق المصغّر (الزجاج السائل)

اعتمد تطبيق الجوال الزجاج السائل لغةً بصرية: القشرة — شريط التطبيق، والأوراق المنبثقة، وشريط التبويب، وبطاقات الجهات — أسطحٌ شفّافة تلتقط ما خلفها وتلمع من حافّتها. هذا القسم يقول لك بالضبط ما نصيبك من ذلك، وما ليس نصيبك، ولماذا.

الخلاصة في سطر: لا تبني الزجاج — اطلبه. المضيف يرسمه لك عبر package:okta_glass، ويرسمه بمكوّناته هو، فحين تعيد أوكتا تصميم المادة يتغيّر شكل تطبيقك عند أول إصدار مضيف بلا أن تنشر شيئاً.

القسمة: ما نرسمه نحن وما ترسمه أنت

الطبقة مَن يرسمها ملاحظة
الحقل البنفسجي خلف كل شيء المضيف لا تصل إليه ولا تحتاجه
شريط التطبيق فوق تطبيقك المضيف (أو OktaAppBar) داكن مصمت في الوضعين — الشكل الثابت أعلاه
ورقة الأذونات، والتنبيهات المضيف تظهر فوق شاشتك بلا استئذانك
مساحة تطبيقك أنت بالمادة عبر OktaGlass، أو مصمتة عبر OktaSurface
بطاقاتك وصفوفك وأزرارك أنت بألوان OktaPalette

package:okta_glass — المادة، جاهزة

import 'package:okta_glass/okta_glass.dart';

Scaffold(
  backgroundColor: const Color(0x00000000),
  appBar: OktaAppBar.build('حضور اليوم', palette),
  body: OktaGlass.background(          // الحقل خلف الشاشة كلّها
    ListView(
      padding: const EdgeInsets.all(16),
      children: <Widget>[
        OktaGlass.card(                // لوح بوزن البطاقة
          OktaList.row(OktaIcons.calendar(), 'الحصة الأولى', '٧:٣٠', palette),
        ),
      ],
    ),
  ),
)
النداء الوزن متى
OktaGlass.card(child) طمس ‎22‎ · نصف قطر ‎20‎ · مرتفع البطاقة — وهو ما تريده في الغالب
OktaGlass.chrome(child) طمس ‎30‎ · نصف قطر ‎26‎ ما يطفو فوق محتوى يتحرّك تحته
OktaGlass.field(child) طمس ‎18‎ · نصف قطر ‎16‎ · مسطّح حقل إدخال — يجب أن يُقرأ غائراً
OktaGlass.surface(r, b, e, child) كما تحدّد عند الضرورة وحدها
OktaGlass.background(child) — الحقل خلف جسم الشاشة

حقل واحد لكل شاشة، لا حقل لكل بطاقة. الحقل هو ما يجعل كل الألواح تتّفق على مصدر الضوء؛ حقلٌ لكل بطاقة يعطي كلّاً منها شمساً خاصة بها.

وطبقتان كحدٍّ أقصى. الثالثة تفقد الحوافّ تباينها ويصير كل شيء ضبابياً.

لماذا يرسمه المضيف ولا ترسمه أنت

الجسر لا يملك أدواته. الزجاج خمس طبقات، وثلاث منها لا تعبر dart_eval:

  • BackdropFilter وImageFilter.blur ليسا في مفردات الجسر أصلاً — لا طمس.
  • BoxDecoration يحمل color وboxShadow فقط؛ وgradient معلَّقة في الجسر، والمعامل غير المعلَن يُسقَط بصمت. فالصبغة المائلة تُترجَم نظيفةً ولا تصل Flutter أبداً — تحصل على مستطيل مسطّح بلا خطأ واحد، وvalidate.dart لا يمسكها لأن شيئاً لم يفشل.
  • BoxDecoration(border: …) لا يُترجَم أصلاً — لا حافّة لامعة.

وهذه الوساطة هي الميزة لا الكلفة. الرسم في ثنائيّة المضيف، لا في بايتكودك. فحين تعيد أوكتا تصميم المادة — لوناً، أو طمساً، أو حافّة — يصل التغيير إلى كل تطبيق منشور عند أول إصدار مضيف: بلا إعادة بناء، ولا إصدار جديد، ولا سطر تعدّله. ومن بنى الشكل بيده من ClipRRect وColoredBox يبقى مجمّداً عند اليوم الذي شحن فيه.

package:okta_glass يتطلّب min_contract: 16 فأعلى. أعلِنه في البيان، وإلا فشلت الترجمة على جهازٍ يحمل مضيفاً أقدم.

وميزةُ عقدٍ ما لا تصير حقيقية إلا حين يصل جهازَ المستخدم بناءُ مضيفٍ يحملها. رفعُ min_contract قبل ذلك لا يجعل تطبيقك أحدث — يجعله يرفض العمل على كل جهاز في الميدان حتى ينزل التحديث من المتجر. تحقّق من عقد المضيف المنشور في لوحة الشريك قبل أن تعتمد عليه.

اللوحة — المصدر الوحيد للون

لا تكتب قيمة لونية حرفية أبداً. Theme.of(context).colorScheme لا يعبر الجسر، فاللوحة هي طريقك الوحيد إلى هوية المضيف:

import 'package:okta_host/okta_host.dart';
import 'package:okta_kit/okta_kit.dart';

Widget main() => const HomeScreen();

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // ابنِها مرّة عند الجذر ومرّرها لأسفل. لا تعِد بناءها في كل ودجت.
    final palette = OktaPalette.of(Okta.isDark());

    return Scaffold(
      backgroundColor: palette.surface,
      appBar: OktaAppBar.build('جدول اليوم', palette),
      body: /* … */,
    );
  }
}
الدور الحقل استعمله لـ
العلامة palette.brand الأزرار الأساسية، التبويب المحدَّد، الأيقونة المميّزة
العلامة الخفيفة palette.brandSoft أثر اللمس، خلفية حالة مرور
على العلامة palette.onBrand نصّ وأيقونة فوق brand
الأرضية palette.surface خلفية الشاشة — Scaffold.backgroundColor
السطح المرفوع palette.surfaceRaised البطاقات والكتل التي تجلس على الأرضية
النص القوي palette.textStrong العناوين والجسم
النص الخافت palette.textMuted الوصف والبيانات الثانوية
الحدّ palette.border الخطوط الشعرية

brand ليس اللون نفسه في الوضعين: ‎#6D428F‎ في الفاتح و‎#A57BBA‎ في الداكن. بنفسجيّ العلامة المشبع فوق أرضية شبه سوداء يتوهّج ويؤذي القراءة. اقرأه من اللوحة ولا تثبّته.

هذه القيم مرآةٌ لـOktaTokens في تطبيق الجوال، ويحرسها اختبار okta_kit_palette_parity_test.dart هناك: إن انحرفت نسخةٌ عن الأخرى سقط البناء. فما تقرأه هنا هو ما يعمل على الجهاز فعلاً.

وصفات تعطيك شكل أوكتا رغم قيود الجسر

الطريق إلى شكل المضيف ليس نسخ ودجتاته — بل مطابقة أرقامه بالأدوات التي تعمل.

بطاقة مدوّرة. لا BoxDecoration(borderRadius:) — تُسقَط بصمت وترسم زوايا قائمة. الزوايا من ClipRRect:

Widget card(OktaPalette palette, Widget child) {
  return ClipRRect(
    borderRadius: BorderRadius.circular(16),  // radiusXl — نصف قطر البطاقة
    clipBehavior: Clip.antiAlias,
    child: ColoredBox(
      color: palette.surfaceRaised,
      child: Padding(
        padding: const EdgeInsets.all(16),    // space4
        child: child,
      ),
    ),
  );
}

خطّ شعري. لا Border.all — لا يُترجَم. الخطّ صندوقٌ بارتفاع واحد:

Container(height: 1.0, color: palette.border)

ارتفاع — وفخٌّ يستحقّ الانتباه. boxShadow مدعوم، لكن الظلّ والزاوية المدوّرة لا يجتمعان: التدوير من ClipRRect وحده، والظلّ من BoxDecoration وحده، وBoxDecoration بلا borderRadius. فظلٌّ خلف بطاقة مدوّرة يُرسم مربّعاً ويظهر عند كل زاوية.

فإمّا سطح مسطّح مدوّر يفصله لونه (OktaSurface.card يفعل هذا)، وإمّا OktaGlass.card الذي ينال الطمس والحافّة والظلّ معاً لأن المضيف يرسمه. الظلّ اليدوي وحده يصلح لصندوق قائم الزوايا لا غير.

الزرّ الأساسي — يُبنى ولا يُضبَط. ElevatedButton وTextButton لا يقبلان style على هذا المحرّك؛ المعامل غير معلَن في الجسر، فتلوينُك يُسقَط بصمت ويصل الزرّ رمادياً افتراضياً. الزرّ الحامل لهويّتك يُركَّب:

Widget primaryButton(OktaPalette palette, String label, void Function() onTap) {
  return GestureDetector(
    onTap: () => onTap(),        // إغلاق لا إحالة دالّة
    child: ClipRRect(
      borderRadius: BorderRadius.circular(999),
      clipBehavior: Clip.antiAlias,
      child: ColoredBox(
        color: palette.brand,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
          child: Center(
            child: Text(
              label,
              style: TextStyle(
                color: palette.onBrand,
                fontSize: 14,
                fontWeight: FontWeight.w600,
              ),
            ),
          ),
        ),
      ),
    ),
  );
}

استخدم ElevatedButton للفعل الثانوي الذي لا يهمّ لونه، وهذا للفعل الأساسي.

محاذاة النصّ. Text لا يقبل textAlign هنا، وTextAlign ليس نوعاً مجسوراً أصلاً. المحاذاة من Center أو Align حول النصّ لا من داخله.

الأرقام التي تجعلك تبدو منّا. طابِقها ولا تخترع غيرها:

القيمة
نصف قطر البطاقة 16 — وللكبسولة 999
حشوة البطاقة 16 · والمضغوطة 12
إيقاع المسافات مضاعفات ‎4‎: ‎4 · 8 · 12 · 16 · 24 · 32‎
مقاس النص ‎12‎ خافت · ‎14‎ ثانوي · ‎16‎ جسم · ‎18‎ عنوان قسم · ‎20‎ عنوان شاشة
الأوزان ‎400‎ جسم · ‎500‎ تسمية · ‎600‎ عنوان — ولا ‎700‎
مدد الحركة ‎150‎ سريع · ‎220‎ أساسي · ‎320‎ بطيء

الحركة. استخدم package:okta_motion — منحناها easeOutCubic هو منحنى المضيف نفسه، فلا يبدو تطبيقك أسرع أو أبطأ من الشاشة التي فُتح منها:

import 'package:okta_motion/okta_motion.dart';

OktaMotion.entrance(index * 40, 220, card(palette, row))

entrance(delayMs, ms, child) هي التتابع نفسه الذي تدخل به بطاقات الرئيسية: ‎40ms‎ بين الصفّ والذي يليه. ثبّت التأخير عند عشرة صفوف — ذيل جدول طويل لا يجوز أن يصل متأخّراً بثانيتين.

الأيقونات. OktaIcons.* فقط. ثوابت Icons.* الاعتباطية غير مجسورة، والأخطر منها أن التطبيق يُبنى بـ--no-tree-shake-icons لأن رموزك تُختار وقت التشغيل: أيقونة خارج المجموعة قد تصل الجهاز مربّعاً فارغاً بلا خطأ.

اتجاه النص

المضيف عربيّ أولاً. AlignmentDirectional غير مجسور، فالاتجاه يأتي من Directionality ومن OktaText:

final locale = Okta.locale();

Directionality(
  textDirection: OktaText.isRtl(locale) ? TextDirection.rtl : TextDirection.ltr,
  child: /* … */,
)

Text(OktaText.pick(locale, 'جدول اليوم', "Today's schedule"))

وقاعدة واحدة تُنسى دائماً: المعرّفات والمسارات وأرقام الإصدار تبقى LTR مهما كانت لغة الواجهة — لُفّها بـDirectionality(textDirection: TextDirection.ltr) وإلا انقلبت 04:A2:9F على الشاشة.

الوضع الداكن ليس اختيارياً

Okta.isDark() يتبع مظهر النظام على جهاز المستخدم. تطبيقٌ يقرأ اللوحة مرّة عند الجذر يحصل على الوضعين مجاناً؛ وتطبيقٌ يكتب Color(0xFF...) مرّةً واحدة يكون صحيحاً في وضع وخاطئاً في الآخر — وهذا أشيع سبب لرفض المراجعة البصرية.

قائمة الفحص قبل النشر

  • لا قيمة لونية حرفية في الكود — كلّها من palette.*
  • Scaffold.backgroundColor = palette.surface
  • البطاقات على palette.surfaceRaised، والزوايا من ClipRRect لا BoxDecoration
  • لا Border.all ولا BoxDecoration(gradient:) — الأولى لا تُترجَم والثانية تُسقَط بصمت
  • لا محاولة لبناء زجاج يدوياً — استخدم OktaGlass (وmin_contract: 16)
  • لا ظلّ خلف زاوية مدوّرة — يُرسم مربّعاً
  • لا ElevatedButton(style:) ولا Text(textAlign:) — كلاهما يُسقَط بصمت
  • المكوّنات من OktaButton/OktaSurface/OktaList/OktaStates لا مبنيّة يدوياً
  • الأيقونات من OktaIcons حصراً
  • فُتِح في الوضعين الفاتح والداكن، وقُرِئ النصّ في كليهما
  • المعرّفات والمسارات ملفوفة LTR
  • الحالات الثلاث معالَجة صراحةً: فارغ، تحميل، خطأ

برومبت جاهز لمساعدك البرمجي

انسخه كما هو إلى Claude Code أو Cursor أو Codex قبل أن تطلب منه شاشة:

تبني تطبيقاً مصغّراً بلغة Dart داخل تطبيق أوكتا للجوال، يُترجَم على
الجهاز بـ dart_eval. التزم بالتالي حرفياً:

الهوية:
- كل لون من OktaPalette.of(Okta.isDark()) — لا Color(0xFF..) حرفي.
- الأرضية palette.surface، البطاقات palette.surfaceRaised،
  النص palette.textStrong / palette.textMuted، الخطوط palette.border.
- نصف قطر البطاقة 16، الكبسولة 999، الحشوة 16، والمسافات مضاعفات 4.
- مقاسات النص 12/14/16/18/20، والأوزان 400/500/600 فقط.
- الأيقونات من OktaIcons فقط.
- المكوّنات من okta_kit ولا تبنِها يدوياً: OktaButton.primary/secondary،
  OktaSurface.card/hairline/pill/iconTile، OktaList.row/sectionHeader،
  OktaStates.loading/empty/failure، والأرقام من OktaMetrics.
- الحركة من package:okta_motion، ومدد 150/220/320.

قيود المحرّك — كلّ بند منها يكسر:
- BoxDecoration يحمل color و boxShadow فقط. borderRadius و gradient
  تُسقَط بصمت: دوّر الزوايا بـ ClipRRect + ColoredBox.
- BoxDecoration(border:) لا يُترجَم: الخطّ الشعري Container(height: 1.0).
- لا BackdropFilter ولا ImageFilter. المادة من package:okta_glass:
  OktaGlass.card / chrome / field / background (يتطلّب min_contract 16).
- الظلّ والزاوية المدوّرة لا يجتمعان — استخدم OktaGlass.card للارتفاع.
- الأزرار: ElevatedButton و TextButton فقط، و**لا تقبلان style**: ابنِ
  الزرّ الأساسي بـ GestureDetector + ClipRRect + ColoredBox(palette.brand).
- Text لا يقبل textAlign: حاذِ بـ Center أو Align حوله.
- التخطيط: Row و Column و Expanded — ومرّر flex: دائماً. لا Wrap ولا
  AlignmentDirectional.
- لا حلقات متداخلة إطلاقاً — سطّح البيانات في قائمة واحدة.
- الـ callbacks إغلاقات: onPressed: () => f() لا onPressed: f.
- أبقِ JSON dynamic من طرف لطرف؛ لا تُعطِ المستقبِل نوع Map.
- لا State.mounted، ولا Timer (لكن DateTime و Duration و Future.delayed
  موجودة، فحلقة السحب استدعاء ذاتي بعد await Future.delayed(مدّة) مع علم
  إيقاف). ولا تمرّر دالّة إلى Future.delayed: تُترجَم ولا تُنادى أبداً.
- GestureDetector لا InkWell — و InkWell و InkResponse و SafeArea
  تُترجَم ثم ترمي UnimplementedError.

ما يقتل التطبيق عند المستخدم — التزم به حرفياً:
- الهوية بالقيم القياسية: Okta.locale() / tenantId() / roleId() / isDark().
  Okta.context() خريطة بمفاتيح snake_case، و .locale عليها غير موجود.
- Okta.uploadFile و location و preciseLocation تعيد **خرائط**:
  ['file_name'] و ['latitude'] — لا .fileName ولا .latitude.
- لا تقرأ مفتاحاً قد يغيب: اجعل الخادم يُصدِر كل حقل دائماً (null حين
  لا قيمة). المفتاح الغائب يعطي null خاماً، و `is` عليه ترمي و `== null`
  تكذب — ولا containsKey (جُرِّب ورُدَّ). OktaJson يحمي من الشكل لا الغياب.
  والفحص الوحيد الذي يجيب عن الغياب identical(value, null) — اسأله قبل
  أن تنادي على القيمة أو تستوفيها أو تخزّنها.
- setState بجسم أقواس: setState(() { x = y; }) لا setState(() => x = y).
- لا تقرأ bool من قائمة مباشرةً: flags[i] == true.
- لا تُهيّئ عدّاد حلقة من معامل، ولا تمرّر عدداً عبر معامل إلى بانٍ مجسور.
- لا تعشّش الثلاثيّات — فرّع وأعِد.
- عنصرٌ من JSON لا يمرّ عبر ?: — rows.isEmpty ? null : rows[0] يفكّ
  صندقته فيموت OktaJson بعده. أسنِد داخل if، أو اقرأ داخل الفرع.
- قبل أي نداء عقده أعلى من 5: احرس بـ Okta.contract() >= N أو أعلِن
  minContract — النداء فوق العقد يُتلف المفسّر لبقية الجلسة ولا يفشل.
- list.sort() بلا مقارِن ترمي: مرّر المقارِن دائماً.

الاتجاه: عربي أولاً عبر Directionality و OktaText.isRtl، والمعرّفات
والمسارات تبقى LTR.

عالج الحالات الثلاث صراحةً: فارغ، تحميل، خطأ. وافحص res.ok، وميّز
status == 0 (لم يصل الخادم) عن ردّ خادم فاشل.

شغّل dart run tool/audit_bridge_usage.dart lib/main.dart قبل النشر — يمسك المعاملات المُسقَطة بصمت والثلاث مصائد أعلاه قبل أن يمسكها المستخدم.

أداة component_catalog في خادم MCP تعرض المجموعة نفسها مُحدَّثة من المنصّة مباشرة — إن كان مساعدك موصولاً بها فاسأله قبل أن تفترض.

عرض الدليل كاملاً في صفحة واحدة · ↧ Markdown خام