الجودة والتطوير

نظام التصميم وواجهة المستخدم

التطبيق المدمج (Embedded) يظهر داخل لوحة المستأجر جنباً إلى جنب مع شاشات أوكتا الأصلية. لذلك أي ميزة بصرية يبنيها شريك يجب أن تتسق مع هوية أوكتا حتى لا يشعر المستأجر بقفزة بصرية. هذا القسم هو المرجع الوحيد الذي تحتاجه أنت — أو أداة AI مثل Claude Code / Cursor / Codex — لبناء واجهة احترافية متناغمة مع المنصة.

اقتباس سريع: في نهاية القسم برومبت جاهز انسخه إلى أي مساعد AI ليبني لك ملفات Livewire + Blade بنفس هويّة أوكتا، بدون أن تحتاج لشرح النظام كل مرة.

المبادئ الأساسية

  1. استخدم المكوّنات الموجودة، لا تخترع مكوّنات جديدة. كل شيء موصوف في "كتالوج المكوّنات" أدناه (<x-card>, <x-button>, <x-badge>, <x-input-field>, <x-textarea>, <x-alert>, <x-spinner>, <x-modal-card>). كل مكوّن آخر هو خروج عن الهوية.
  2. Tailwind tokens من الـpalette أدناه للرماديات وألوان الحالة. ولونك الخاص له مكان مدعوم — راجع «مساحة الإبداع» أدناه؛ الممنوع هو hex حرفيّ بلا مقابل داكن.
  3. RTL/LTR على مستوى الـlogical properties: start/end, ms-/me-, ps-/pe-, text-start/text-end. الأسهم والـchevron تُدوَّر بـrtl:rotate-180.
  4. font-mono و dir="ltr" لكل المعرّفات والـURLs والـslugs.
  5. Card-based composition: كل قسم في <x-card> بحدود ناعمة وظِلّ خفيف.
  6. States أوّليّة: empty / loading / error دائماً معالَجة بشكل صريح، ليست fallback.
  7. Save bar واحد ثابت بدل أزرار حفظ متعدّدة لكل قسم فرعي.
  8. الحركة من طبقة motion-* لا من keyframes خاصّة — فمفتاح prefers-reduced-motion مطبَّق عليها مرّة واحدة (راجع «مساحة الإبداع»).

لاختصار التحديث على prod، باقي محتوى قسم التصميم (Brand tokens، كتالوج x-*، wire-elements/modal، Layout rules، البرومبت الجاهز، نسخ مرجعية) كما هو على main commit 40ae5c0 و prod commit 5e29d0f. أي إضافة مستقبلية لقواعد التصميم تذهب هناك أولاً ثم تُنسَخ هنا.

مساحة الإبداع — ما تملك تغييره

القواعد أعلاه تحمي الأساس: التوكنات، والوضع الداكن، و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 من الكود.


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