الجودة والتطوير
نظام التصميم وواجهة المستخدم
التطبيق المدمج (Embedded) يظهر داخل لوحة المستأجر جنباً إلى جنب مع شاشات أوكتا الأصلية. لذلك أي ميزة بصرية يبنيها شريك يجب أن تتسق مع هوية أوكتا حتى لا يشعر المستأجر بقفزة بصرية. هذا القسم هو المرجع الوحيد الذي تحتاجه أنت — أو أداة AI مثل Claude Code / Cursor / Codex — لبناء واجهة احترافية متناغمة مع المنصة.
اقتباس سريع: في نهاية القسم برومبت جاهز انسخه إلى أي مساعد AI ليبني لك ملفات Livewire + Blade بنفس هويّة أوكتا، بدون أن تحتاج لشرح النظام كل مرة.
المبادئ الأساسية
- استخدم المكوّنات الموجودة، لا تخترع مكوّنات جديدة. كل شيء
موصوف في "كتالوج المكوّنات" أدناه (
<x-card>,<x-button>,<x-badge>,<x-input-field>,<x-textarea>,<x-alert>,<x-spinner>,<x-modal-card>). كل مكوّن آخر هو خروج عن الهوية. - Tailwind tokens من الـpalette أدناه للرماديات وألوان الحالة. ولونك الخاص له مكان مدعوم — راجع «مساحة الإبداع» أدناه؛ الممنوع هو hex حرفيّ بلا مقابل داكن.
- RTL/LTR على مستوى الـlogical properties:
start/end,ms-/me-,ps-/pe-,text-start/text-end. الأسهم والـchevron تُدوَّر بـrtl:rotate-180. - font-mono و dir="ltr" لكل المعرّفات والـURLs والـslugs.
- Card-based composition: كل قسم في
<x-card>بحدود ناعمة وظِلّ خفيف. - States أوّليّة: empty / loading / error دائماً معالَجة بشكل صريح، ليست fallback.
- Save bar واحد ثابت بدل أزرار حفظ متعدّدة لكل قسم فرعي.
- الحركة من طبقة
motion-*لا من keyframes خاصّة — فمفتاحprefers-reduced-motionمطبَّق عليها مرّة واحدة (راجع «مساحة الإبداع»).
لاختصار التحديث على prod، باقي محتوى قسم التصميم (Brand tokens، كتالوج x-*، wire-elements/modal، Layout rules، البرومبت الجاهز، نسخ مرجعية) كما هو على main commit
40ae5c0و prod commit5e29d0f. أي إضافة مستقبلية لقواعد التصميم تذهب هناك أولاً ثم تُنسَخ هنا.
مساحة الإبداع — ما تملك تغييره
القواعد أعلاه تحمي الأساس: التوكنات، والوضع الداكن، وRTL، وإتاحة المدخلات. وهي لا تعني أن تبدو كل التطبيقات نسخة واحدة. هذا القسم هو ما تملكه فعلاً، ووُجد لأن أغلب أسباب هجر الشركاء لنظام التصميم كانت خيارات موجودة أصلاً أو كان يجب أن توجد.
اسأل مساعدك أولاً: أداة
component_catalogفي MCP تعرض كل مكوّن بخصائصه وأمثلته، وأصناف الحركة، ولون تطبيقك، وأي قواعد الفاحص تُرسِب وأيّها لا.
١. البطاقة صارت مرنة
<x-card> كان له شكل واحد، فمن أراد غيره ترك المكوّن وبنى div — فخسر
التوكنات والوضع الداكن ليكسب شكلاً. الآن الشكل من المكوّن:
<x-card tone="accent" padding="none" overflow="visible" elevation="lifted">
| الخاصية | القيم | متى |
|---|---|---|
tone |
raised (افتراضي) · sunken · plain · accent |
plain = تجميع بلا صندوق مرئي؛ accent = بلون تطبيقك |
elevation |
flat · raised · lifted |
lifted يرتفع عند المرور |
padding |
default · tight · none |
none للجدول والخريطة والشريط الإعلامي الذي يجب أن يمتدّ لحافة البطاقة |
radius |
default · lg · none |
|
divider |
true (افتراضي) · false |
الخطّ تحت الترويسة |
overflow |
hidden (افتراضي) · visible |
لقائمة منسدلة تخرج من البطاقة |
كلّها اختيارية، والافتراضي يُنتج الشكل القديم حرفياً — فلا يتغيّر شيء قائم.
٢. لونك أنت
المسار الوحيد للون خاص كان قيمة حرفية bg-[#0F766E]. ولها قيمة واحدة، فهي
صحيحة في وضع وخاطئة في الآخر — والمنصّة لا تستطيع مساعدتك لأن hex داخل صنف
غير مقروء لها.
أعلِنه في البيان بقيمتين، فتصير القيمة معلومة:
"brand": {
"accent": { "light": "#0F766E", "dark": "#2DD4BF" },
"accent_contrast": { "light": "#FFFFFF", "dark": "#042F2E" },
"accent_soft": { "light": "#CCFBF1", "dark": "#134E4A" }
}
ثم استعمله كمتغيّر — يتبع الوضع الداكن كما تفعل توكنات المنصّة:
<div class="bg-[var(--app-accent)] text-[var(--app-accent-contrast)]">…</div>
<x-card tone="accent">…</x-card>
- hex فقط (٣ أو ٦ أو ٨ خانات). القيمة تُكتَب داخل
<style>، فأي صيغة أخرى —rgb(...)،var(...)،color-mix(...)— تُسقَط بلا محاولة تهريب. - ثلاثة أدوار لا غير. قائمة أطول تصير نظام ثيمات ثانياً، وعندها يفقد نظام المنصّة معناه.
- محصور في صفحاتك عبر
[data-app-brand]— لا يبلغ كروم المنصّة ولا تطبيقاً آخر على الشاشة نفسها. - أعلنت
lightوحدها؟ الداكن يرث القيمة نفسها، فلا يبقى نصف مطليّ. - دور معطوب يسقط وحده ولا يُسقِط البقيّة.
٣. الحركة
طبقة جاهزة بدل كتابة @keyframes:
| المجموعة | الأصناف |
|---|---|
| دخول | motion-fade · motion-rise · motion-slide-in · motion-scale-in |
| لفت انتباه | motion-pop · motion-shake · motion-pulse |
| تفاعل | motion-hover-lift · motion-press |
| تعديل | motion-slow · motion-delay-1..3 |
<div class="motion-stagger">
@foreach ($rows as $row)
<x-card class="motion-rise">…</x-card>
@endforeach
</div>
motion-stagger على الحاوية يجعل كل ابن يدخل بعد سابقه بـ40ms — حتى عشرة ثم
يثبت، فذيل جدول طويل لا يصل متأخّراً بثانية ونصف.
ولماذا تستعملها بدل keyframes خاصّة بك: prefers-reduced-motion مطبَّق
عليها كلّها في مكان واحد. ومَن أوقف الحركة في نظامه لم يفعل ذلك ذوقاً — لصاحب
اضطراب دهليزي، حركة لم يطلبها عَرَض لا زينة. كل keyframes تكتبها بنفسك موضع
إضافي يجب أن يتذكّره.
وأعِد التوقيت متى شئت: --motion-duration على أي عنصر يعيد توقيت كل ما تحته،
و0ms توقفه.
٤. الفاحص قناتان لا واحدة
- يُرسِب البناء — ما ينكسر خارج تصميمك:
<input>/<select>/<textarea>الخام، ورمادياتgray|zinc|slate|indigo، وmax-w-*بمقاييس الحاويات (3xlفأعلى،full،screen-*). - يُذكَر ولا يُرسِب — اتساق تملك تجاوزه:
<button>خام، ولون حرفي.
وmax-w-prose وmax-w-[42ch] ليستا مخالفتين — تلك طباعة لا حاوية.
والجدول الخام لم يعد مخالفة إطلاقاً: <x-table> لا يعبّر عن جدول حصص ولا
تقويم، وقاعدة يتخطّاها الجميع تُعلِّم تخطّي القواعد لا احترامها.
٥. النوافذ (Modals)
لا تبنِ نافذة بنفسك بـx-show/x-data وطبقة تعتيم يدوية. المنصّة
تستعمل wire-elements/modal، والحزمة مسجَّلة في التخطيط أصلاً — لا تُسجّلها
ثانيةً.
١. المكوّن يرث ModalComponent لا Component:
use LivewireUI\Modal\ModalComponent;
class WithdrawalRequestModal extends ModalComponent
{
public int $moduleId;
public static function modalMaxWidth(): string
{
return 'lg'; // sm | md | lg | xl | 2xl …
}
public function mount(int $moduleId): void
{
$this->moduleId = $moduleId;
}
public function submit(): void
{
// …
$this->closeModal();
}
}
٢. القالب يستعمل <x-modal-card> دائماً — لا <header> ولا <footer>
خاماً:
<x-modal-card class="w-[min(92vw,32rem)]" :title="__('…')" :subtitle="__('…')">
<form wire:submit="submit" id="my-form" class="space-y-3">
<x-input-field :label="__('…')" wire:model="amount"
:errorMessage="$errors->first('amount')" />
</form>
<x-slot:footer>
<x-button variant="primary" type="submit" form="my-form">{{ __('actions.save') }}</x-button>
<x-button variant="secondary-subtle" type="button"
wire:click="$dispatch('closeModal')">{{ __('actions.cancel') }}</x-button>
</x-slot:footer>
</x-modal-card>
<x-modal-card> يعطيك الترويسة وزرّ الإغلاق والحدود والحشوة و<x-slot:footer>
بفاصل علوي. ولاحظ w-[min(92vw,32rem)] لا max-w-*: العرض المسؤول للنافذة
يُكتب هكذا.
الزرّ خارج
<form>ويشير إليه بـform="my-form"— فالفوتر شقيق للنموذج لا ابنه، وزرّsubmitخارج نموذجه لا يرسله بلا هذه الإشارة.
٣. الفتح من أي مكان بحدث، مع الوسائط التي يستقبلها mount():
wire:click="$dispatch('openModal', {
component: 'partner.modules.new-version-modal',
arguments: { moduleId: {{ $module->id }} }
})"
الاسم هو alias المكوّن بالصيغة المسطَّحة. تجنّب :: فيه — Livewire 4
يمرّر الاسم على normalizeName() قبل البحث، فالـ alias الذي يحوي :: لا
يُحَلّ أبداً.
٤. الإغلاق: $this->closeModal() من الخادم، أو
wire:click="$dispatch('closeModal')" من القالب.
لماذا لا تبني نافذتك: طبقة التعتيم وحبس التركيز وإغلاق Escape وإعادة
التركيز إلى الزرّ الذي فتحها ومنع تمرير الخلفية — كلّها في الحزمة. النافذة
المبنيّة يدوياً تبدو صحيحة وتترك مستخدم لوحة المفاتيح داخل الصفحة خلفها.
الرسوم البيانية — <x-chart>
مكوّن واحد لكل رسم بياني، والمكتبة (ApexCharts) مُدمَجة داخل المنصّة —
لا تحمّل Chart.js ولا ApexCharts من CDN: تطبيقك يعمل داخل صفحة المنصّة، وسكربت
من الخارج يعمل خارج حزمتها وسياسة أمانها ويرسم بألوان لا تعرف الثيم. الفاحص
يُرسِب البناء عليه (remote-chart-library).
{{-- من مكوّن Livewire: مرّر البيانات كما تخرج من الاستعلام --}}
<x-chart
type="bar"
:series="['حضور' => $present, 'غياب' => $absent]"
:categories="$days"
:title="__('my-app::stats.attendance')"
format="percent"
height="280"
/>
<x-chart type="donut" :series="['حاضر' => 42, 'غائب' => 3, 'متأخّر' => 5]" />
<x-chart type="line" :series="[['name' => 'الرسوم', 'data' => $fees]]" :categories="$months" format="currency" currency="SAR" />
type:line(افتراضي) ·area·bar·column·scatter·radar·heatmap·treemap·pie·donut·radial.seriesيقبل ما يخرج من استعلامك بلا إعادة تشكيل: قائمة أرقام، قائمة مفتاحية (المفاتيح تصير الفئات أو الشرائح)، خريطة اسم←قائمة (سلسلة لكل مفتاح)،[{name, data}]، ونقاط{x, y}للـ scatter. الدائرية تقبل[{name, value}]أيضاً.nullفجوة على الخطّ لا صفر.format:number·currency(+currency="SAR") ·percent، معprecisionللمنازل. الأرقام تُنسَّق بلغة الصفحة.- الألوان من tokens المنصّة: لون تطبيقك (من
brandفي البيان) أولاً ثم لوحة المنصّة، ونصّ المحاور والشبكة من الـ tokens نفسها — فالرسم يتبع الوضع الداكن وRTL تلقائياً ويُعاد رسمه عند تبديل الثيم.:colorsتتجاوز اللوحة إن احتجت. - التحديث الحيّ بلا كود: مرّر
:seriesمن خاصية أو دالة في مكوّن Livewire؛ مع كل إعادة تصيير يتحدّث الرسم في مكانه بانتقال متحرّك. لا حدث ولاentangle. - الفراغ مُعالَج: قيم كلها
null(أو مجموع دائري صفر) تُظهر حالة فارغة (emptyTextلتغيير النصّ). الصفر على خطّ قيمة لا غياب. :optionsمخرج الطوارئ: أيّ خيار ApexCharts خام يُدمَج آخراً — قبل أن تكتبnew ApexCharts(بنفسك (وهو ما يحذّر منه الفاحص:hand-driven-chart).- فتحتان:
<x-slot:actions>بجانب العنوان (فلتر، تبديل مدّة) و<x-slot:footer>.
Toasts
استخدم نظام الـtoasts المشترك بدل alerts مدمجة في الصفحة:
$this->dispatch('toast', message: __('my-module::students.created'));
$this->dispatch('toast', message: __('errors.generic'), type: 'error');
أنواع: success (افتراضي), error, warning, info.
البرومبت الجاهز لمساعد AI
البرومبت الكامل المُوصى به في commit 5e29d0f على prod. ابدأ منه بدون
تغيير. القاعدة #12 فيه تذكر بضرورة تصريح كل مفتاح إشعار في الكتالوج
أعلاه قبل dispatch من الكود.