الورقة السفلية
Bottom SheetBottomSheetDialog / .presentationDetents
اسمه كمانبوتوم شيت، شيت، اللوحة السفلية، اللوح المنزلق من الأسفل، القائمة السفلية، الدرج السفلي، sheet، modal bottom sheet، half sheet، slide-up panel، bottom drawer، pull-up panel
الورقة السفلية سطح ملتصق بالحافّة السفلية للشاشة، يصعد فوق المحتوى ويستقرّ عند ارتفاع من ارتفاعات معدودة، ويستجيب لسحب الإصبع صعوداً وهبوطاً. يقوم تعريفها على شيئين معاً: الحافّة التي تنتمي إليها، وارتفاعها الذي يملكه المستخدم لا المصمّم وحده. ولهذا هي ليست نافذة حوارية (modal dialog) تتوسّط المنطقة المرئية ولا تنتمي إلى حافّة ولا تُسحب ومقياسها الوحيد هو الحَجْب؛ وليست ورقة إجراءات (action sheet)، وهي قائمة أوامر قصيرة أصلها iOS بلا نقاط توقّف ولا محتوى حرّ؛ وليست درجاً (drawer) يخرج من حافّة جانبية فيتحرّك على المحور المضمّن وينعكس مع اتجاه الواجهة. والفارق الحاسم عن الدرج أن الورقة تتحرّك على المحور الكتلي وحده، فاتجاه حركتها لا ينعكس أبداً. وما إن يدخل ورقةَ الإجراءات نموذجٌ أو خريطةٌ أو قائمةٌ طويلة حتى يصير اسمها الصحيح ورقة سفلية. وفي داخل العنصر نفسه فرق ثانٍ لا يقلّ أهمّية: الورقة الحاجبة لها طبقة تعتيم وتُجمّد ما تحتها حتى تُغلق، أمّا الملازمة (persistent) فتبقى ظاهرة بلا تعتيم ويظلّ التطبيق تحتها قابلاً للاستعمال. والفرق بينهما سطر واحد في الكود: presentationBackgroundInteraction على iOS، و BottomSheetScaffold بدل ModalBottomSheet على أندرويد.
لو قلت عليه…
العيّنة الحيّة
تفاعل مع الديمو. كل الأجزاء حقيقية ومرقّمة.
المطاعم القريبة
١٢ مكاناً حولك
مطعم البرج
تشريح العنصر: كل جزء واسمه
مرّر على أي سطر ليتحدّد مكانه في الديمو فوق.
برومبت البناء
سلوكه في الاتجاه من اليمين لليسار
الجزء ده حصري عندنا.
المقبض لا ينعكس
لا ينعكسالمقبض متمركز أفقياً، وشكله متماثل حول محوره الرأسي، فليس فيه ما ينعكس. الفخّ في طريقة التمركز لا في الشكل: inset-inline-start: 50% مع transform: translateX(-50%) تبدو سليمة لأنها تعمل في LTR، لكنها تنكسر في RTL لأن الخاصّية المنطقية صارت تقيس من اليمين بينما translateX ظلّ فيزيائياً، فينزاح المقبض بمقدار عرضه كاملاً إلى الجهة الخطأ. ضَعْه في المنتصف بحاوية flex فيها justify-content: center، أو بـ margin-inline: auto، واترك التموضع المطلق كلّه.
صفّ إجراءات الرأس
ينعكسزوج «إلغاء / تمّ» في رأس الورقة يتبادل الطرفين: إلغاء عند البداية المنطقية وتمّ عند النهاية، فيكون إلغاء يميناً في العربية ويساراً في الإنجليزية. ثبّت ترتيب الـ DOM (إلغاء أوّلاً) واستخدم justify-content: space-between فينعكس الصفّ وحده مع direction، ولا تكتب flex-direction: row-reverse داخل [dir="rtl"] لأن القلب سيقع مرّتين وينفصل الترتيب البصري عن ترتيب التنقّل بلوحة المفاتيح أو بـ VoiceOver. وفي SwiftUI حافّتا leading و trailing منطقيّتان تنعكسان وحدهما متى ورثت الواجهة layoutDirection = .rightToLeft، والذي لا ينعكس هو .offset(x:) لأنه إزاحة فيزيائية، والبديل عنه حشو أو محاذاة.
حواف الأمان فيزيائية
لا ينعكسالدالّتان env(safe-area-inset-left) و env(safe-area-inset-right) تصفان حافّتَي الجهاز نفسه، النتوء وفتحة الكاميرا وانحناء الزجاج، ولا مقابل منطقيّ لهما في CSS، فلا وجود لشيء اسمه env(safe-area-inset-inline-start). ومن يمشّط الملفّ ليستبدل كل left و right بنظيرتها المنطقية يقلب هاتين معهما، فيدفع محتوى الورقة تحت النتوء لحظة يدور الجهاز أفقياً. اتركهما فيزيائيتين، وادمج حشوك أنت في التعبير نفسه: padding-left: max(1rem, env(safe-area-inset-left)) ومثلها لـ padding-right، فلو كتبت padding-inline بعدهما دهس الإعلانُ المتأخّر قيمةَ الأمان وعاد المحتوى إلى تحت النتوء. والقاعدة نفسها على أندرويد: WindowInsets.safeDrawing يسلّمك left و right بوصفهما جهتَي الجهاز لا بداية السطر ونهايته، فطبّقهما كما جاءا ولا تعكسهما بيدك.
السحب الرأسي محايد
لا ينعكسكل حركة الورقة على المحور الكتلي: السحب صعوداً وهبوطاً، ونقاط التوقّف، وعتبة السرعة التي تقرّر الإغلاق. ولا شيء من ذلك يتأثّر بـ direction، فلا تكتب له أي قاعدة داخل [dir="rtl"] ولا تلمس translateY. والخطأ يقع عند من يعامل الورقة معاملة الدرج فيضيف سحباً أفقياً للإغلاق: ذاك محور مضمّن ينقلب معناه بين اللغتين، وعلى iOS يصطدم بإيماءة الرجوع التي موضعها الحافّة البادئة، وهي في الواجهة العربية الحافّة اليمنى. أبقِ الإغلاق على ثلاثة طرق محايدة: السحب لأسفل، وزرّ صريح، والنقر على الطبقة الحاجبة.
ما بداخل الورقة
ينعكسانعكاس الورقة لا يعني أن ما بداخلها انعكس معها. على أندرويد لا ينعكس شيء أصلاً ما لم يكن android:supportsRtl="true" في الـ manifest، وبعدها لا بدّ من paddingStart و layout_marginStart بدل نظائرها اليسارية. وداخل الصفوف: الأيقونة القائدة والشيفرون يتبادلان الطرفين، والشيفرون نفسه اتجاهيّ فاقلبه بـ transform: scaleX(-1) داخل [dir="rtl"] أو بـ rtl:-scale-x-100 في Tailwind، أو خُذ الأيقونة من Icons.AutoMirrored في Compose؛ أمّا النجمة وأيقونات الحالة فلا تنعكس أبداً وقلبها يجعلها تبدو مكسورة. واضبط المحاذاة بـ text-align: start لا left، ولفّ الأسعار وأرقام الهواتف بـ <bdi> أو unicode-bidi: isolate حتى لا ينزلق رمز العملة إلى الطرف الخطأ من السطر العربي.
أسماؤه في الكود
كل سطر هو كلمة مكتبة واحدة عن نفس الشيء. خد السطر اللي بيكلّم مشروعك.
شوف كمان
آخر تحديث · 2026-08-19