هذه الصفحة مترجمة من الإنجليزية. اقرأ الصفحة الإنجليزية
الحالة: مسودة، منفّذة عبر zeyn::server وassets/zeyn/*.js (تُقدَّم بوصفها /_zeyn/zeyn.js).
هذا هو العقد بين خادم Zeyn ووقت تشغيل المتصفح. تُرسم الشاشات على الخادم. ووقت تشغيل المتصفح سكربت صغير يرسل إجراءات المستخدم إلى الخادم ويطبّق ما يعود. ولا تكتب شيفرة التطبيق JavaScript أبدًا.
1. تحميل الصفحة
يُرجع GET {route}?params={json} مستند HTML كاملًا. params هو JSON لمعاملات الشاشة بعد ترميز URL، ويُحذف عندما لا تكون للشاشة معاملات.
المسار ذو المقاطع، مثل /orders/{id}، يقرأ حقول params تلك من المسار (/orders/42) ويقرأ الحقول الأخرى من params. وقيمة المقطع التي لا يمكن تحليلها تجيب بـ 404. تُرمَّز القيم بنسبة مئوية، بما فيها النقاط (%2E)، فلا يبدو عنوان الشاشة ملفًّا لوقت التشغيل أبدًا. والشاشة التي تجيب بـ Screen::moved_to تحصل على 301 Moved Permanently مع العنوان الجديد في Location.
الصفحة الفاشلة تجيب بحالتها (404 أو 403 أو 500 ...) وبشاشة الخطأ الخاصة بالتطبيق (App::not_found وApp::server_error) أو رسالة قصيرة مضمّنة، داخل الهيكل (shell). والطلب الذي يحمل x-zeyn-partial: 1 يحصل على تلك الشاشة فقط مع x-zeyn-partial: 1 وx-zeyn-title، فيبدّلها وقت التشغيل دون تحميل للصفحة. ولا تصل تفاصيل الخطأ إلى المتصفح أبدًا.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="zeyn-csrf" content="TOKEN">
<title>Products · Inventory</title>
<link rel="stylesheet" href="/_zeyn/zeyn.css?v=BUILD">
<script type="module" src="/_zeyn/zeyn.js?v=BUILD"></script>
</head>
<body>
<a class="z-skip" href="#z-main">Skip to content</a>
<div class="z-app">
<nav class="z-nav" aria-label="Main">...</nav>
<main class="z-main" id="z-main">SCREEN</main>
</div>
<div class="z-toasts" role="status" aria-live="polite"></div>
</body>
</html>تسرد القائمة الشاشات التي يحق للمستخدم الحالي فتحها فقط. وللعنصر الحالي aria-current="page".
يحمل كل سكربت وورقة أنماط للإطار في الترويسة ?v= ومعرّف البناء (meta[name=zeyn-build]، من ZEYN_BUILD_ID أو من تجزئة الملف التنفيذي)؛ ويحمل theme.css تجزئة نص السمة. الطلب الذي يحمل القيمة الحالية يحصل على Cache-Control: public, max-age=31536000, immutable، فلا يطلب تحميل صفحة دافئ أيًّا منها؛ وأي طلب آخر (تبويب قديم بعد نشر جديد، أو بلا v) يحصل على no-cache والـ ETag. والوحدات التي يستوردها وقت التشغيل لاحقًا تأخذ v نفسه من عنوان السكربت الذي يستوردها.
تحمّل كل صفحة zeyn.js (الطلبات والتأثيرات والنوافذ والنماذج والقوائم والاختصارات وتفويض الأحداث) وnav.js وcomponents.js، ومعها pro.js وenterprise.js في غلاف التطبيق. ولا يُحمَّل الباقي إلا في الصفحات التي تستخدمه، وكذلك عندما يُدخل nav.js شاشة: يستورد zeyn.js وقت تشغيل الشبكة grid.js أول مرة يحوي فيها المحتوى [data-zeyn-grid] (والصفحات الكاملة ذات الشبكة تسميه في <link rel="modulepreload">)، ويستورد components.js الوحدات catalog.js (شبكات البطاقات ومربعات البحث) وmedia.js (المشغّلات والتضمينات) وfiles.js (الرفع) وcharts.js وassistant.js (لوحات الدردشة، أو أول ضغطة على زر الإطلاق). وتبقى أوراق أنماطها في الترويسة، فلا يتغير المظهر عند وصول السكربت. في لوحة المعرض يعني هذا 5 سكربتات و258 KB (74 KB على الشبكة) بدل 9 سكربتات و403 KB (116 KB).
2. شيفرة الشاشة
يحمل جذر كل شاشة اسمها ومعاملاتها:
<section class="z-screen" data-zeyn-screen="product_list" data-zeyn-params="null" aria-labelledby="product_list-title">تستخدم النوافذ class="z-window" role="dialog" aria-modal="true" ونمط --z-width.
يُغلَّف كل عنصر من الجسم بعنصر عقدة (node). ومساره هو العنوان الذي تستخدمه نقاط النهاية أدناه. وتغلّف الحاويات (Row وStack وPanel وCard وTabs وSplit) كل ابن بالطريقة نفسها، ويضيف مسار الابن فهرسه: الابن الثالث للابن الأول من عنصر الجسم 1 هو "1.0.2".
<div class="z-node" data-zeyn-node="1">
<div class="z-node" data-zeyn-node="1.0">...</div>
</div>تأخذ نقاط النهاية node سلسلةً نقطية أو، لعناصر الجسم، رقمًا عاديًا (0). للمسارات 16 مستوى من الأرقام على الأكثر؛ وأي شيء آخر يُرفض بـ 422.
3. السمات التي يقرؤها وقت التشغيل
| السمة | على | المعنى |
|---|---|---|
data-zeyn-effect | زر، tr | effect بصيغة JSON يُنفَّذ عند النقر (الصفوف: عند النقر المزدوج أو Enter) |
aria-keyshortcuts | زر | اختصار لوحة المفاتيح، مثل Control+N |
data-zeyn-export | زر | صيغة تصدير الشبكة على الشاشة نفسها (csv وxlsx) |
data-zeyn-grid | جذر الشبكة | يعلّم شبكة؛ وفيه أيضًا data-zeyn-mode وdata-zeyn-paging وdata-zeyn-page-size وdata-zeyn-total وdata-zeyn-next وdata-zeyn-offset |
data-field وaria-sort وaria-colindex | th | اسم حقل العمود والفرز الحالي والموضع |
data-filter وdata-options | th | نوع حقل المرشّح (text وnumber وmoney وdate وdatetime وtime وchoice وbool) وخيارات الاختيار [{value,label}] |
data-pin | th | start أو end للأعمدة المثبّتة (وكذلك عمودا التحديد وإجراء الصف) |
data-edit وdata-optional | th | نوع الإدخال لعمود قابل للتحرير، للمستخدمين الذين يحق لهم الحفظ |
data-zeyn-texts وdata-zeyn-columns وdata-zeyn-view | جذر الشبكة | نصوص التحكم المحلّية، وكل عمود [{field,label,hidden,pin,width}]، والعرض المحفوظ |
data-zeyn-views | جذر الشبكة | server عندما تُحفظ العروض على الخادم |
data-zeyn-version-field وdata-zeyn-exportable | جذر الشبكة | اسم حقل إصدار الصف للتحرير المضمّن؛ وcsv عندما يمكن تصدير الشبكة |
data-zeyn-key وdata-zeyn-version وdata-zeyn-menu | tr | مفتاح الصف وإصداره وعناصر قائمة السياق |
data-v | td | قيمة الإدخال لخلية قابلة للتحرير |
data-zeyn-detail وaria-expanded | tr | شاشة تفصيل الصف بصيغة {screen, params}؛ وهل هي مفتوحة |
data-zeyn-expand | زر في صف | يفتح تفصيل الصف أو مجموعته أو أبنائه أو يغلقها |
data-zeyn-level وaria-level وdata-zeyn-path | tr في treegrid | العمق (0 في الأعلى) في شبكة مجمّعة أو شجرية؛ ومسار قيم المجموعات أو مفاتيح الأسلاف الذي يُرسل عند فتح الصف |
z-group | tr | صف ترويسة مجموعة؛ وz-branch-more صف "تحميل المزيد" لفرع |
data-zeyn-run | زر في صف | عنصر الصف المراد تشغيله (a0 هو أول إجراء صف) |
data-agg | tfoot td | الإجماليات التي يعرضها العمود (sum avg min max count) |
data-zeyn-bulk | div قبل الشبكة | شريط التحديد مع أزرار data-zeyn-bulk-run |
data-zeyn-menu | جذر الشاشة | قائمة سياق الشاشة |
data-zeyn-native-menu="off" | html | قائمة سياق المتصفح معطّلة إلا في حقول إدخال النص |
data-zeyn-rows | tbody | موضع الصفوف |
data-zeyn-page | زر المرقّم | prev أو next |
data-zeyn-page-info | span | نص مثل 1-50 of 230، يُبنى من القوالب المحلّية على جذر الشبكة |
data-zeyn-text-range وdata-zeyn-text-range-total وdata-zeyn-text-loaded | جذر الشبكة | قوالب محلّية: {from}-{to} و{from}-{to} of {total} و{count} rows loaded |
data-zeyn-more | div | حارس التمرير اللانهائي، ويحوي زر "تحميل المزيد" |
data-zeyn-submit | form | اسم action الإرسال |
data-zeyn-editor | input | محرّر من جهة العميل لنوع حقل مخصص |
الـ effects:
| Effect | JSON | ما يفعله وقت التشغيل |
|---|---|---|
| فتح نافذة | {"effect":"open_window","screen":"product_form","params":{...}} | يحمّل الشاشة (القسم 4.1) في <dialog> مشروط (modal) |
| تنقّل | {"effect":"navigate","screen":"...","params":{...}} | يحمّل /_zeyn/route/{screen}?params= الذي يعيد التوجيه إلى الصفحة، بصيغة المسار عندما يكون لمسارها مقاطع |
| إشعار | {"effect":"toast","message":"Saved"} | يعرض إشعارًا (toast) لمدة 4 ثوانٍ |
| إغلاق وتحديث | {"effect":"close_and_refresh"} | يغلق أعلى مربع حوار ويعيد تحميل كل شبكة على الشاشة تحته |
4. نقاط النهاية
كل طلبات POST ترسل JSON، ويجب أن تحمل الترويسة x-zeyn-csrf مع الرمز المأخوذ من وسم zeyn-csrf meta، وإلا رُفضت بـ 403.
الشاشة العامة المفتوحة بلا جلسة تأتي بوسم zeyn-csrf meta فارغ وبلا Set-Cookie، حتى يمكن تخزينها مؤقتًا. وقبل أول POST يستدعي وقت التشغيل GET /_zeyn/csrf، الذي يجيب بـ { "token": "..." } مع Cache-Control: no-store، ويضبط ملف تعريف الارتباط zeyn_csrf عندما لا يملكه المتصفح، ويرفض الطلبات التي ليست قيمة Sec-Fetch-Site فيها same-origin. يذهب الرمز إلى وسم meta؛ وعندما يفشل الاستدعاء يخرج الـ POST بلا رمز ويُرفض كأي طلب آخر. ولا يُعاد شيء بصمت.
التنقل داخل التطبيق (nav.js) يطلب شاشة مع x-zeyn-partial: 1 ويحصل على HTML الخاص بها فقط، مع عنوان الصفحة في x-zeyn-title. وعندما تضبط الشاشة وسوم الرأس (الوصف والعنوان القانوني (canonical) وصورة المعاينة وrobots وJSON-LD)، يبدأ الجسم بـ <template data-zeyn-head> يحويها؛ فيزيل nav.js كل عنصر [data-zeyn-head] من رأس المستند، وينقل وسوم القالب إلى هناك، ويُسقط القالب. والشاشة بلا وسوم رأس لا ترسل قالبًا، فلا تُزال إلا الوسوم القديمة.
4.1 جزء الشاشة
يُرجع GET /_zeyn/screen/{screen}?params={json} شيفرة HTML للشاشة فقط، بلا هيكل الصفحة. يُستخدم للنوافذ ولتفاصيل الصفوف: فتح صف له data-zeyn-detail يحمّل شاشته في tr.z-detail جديد تحته مباشرةً (خلية واحدة تمتد على الصف، وتحوي role="region")، وإغلاقه يزيل ذلك الصف. وتُفحص صلاحية الشاشة كأي شاشة؛ وتتحدث الشبكات داخل التفصيل إلى الخادم باسم شاشة التفصيل نفسها ومعاملاتها.
4.2 صفوف الشبكة
POST /_zeyn/grid
{
"screen": "product_list",
"params": null,
"node": 0,
"query": {
"page": { "type": "offset", "offset": 0, "limit": 50 },
"sort": [{ "field": "code", "desc": false }],
"filters": [{ "field": "name", "contains": "tea" }, { "field": "price", "from": "10", "to": "99.90" }],
"with_total": true,
"aggregates": [{ "field": "price", "op": "sum" }]
},
"view": { "columns": [{ "field": "code", "width": 140, "pin": "start" }, { "field": "name" }] },
"head": false
}قد تضبط المرشّحات contains وstarts_with وequals وfrom وto وany_of وis (انظر docs/grid.md). view.columns هو تخطيط الأعمدة الذي تُرسم الصفوف به؛ ومع head: true تحمل الاستجابة أيضًا head، أي صف الترويسة.
للتمرير اللانهائي تكون page هي { "type": "after", "cursor": "...", "limit": 100 } وتكون cursor قيمة null للدفعة الأولى.
الاستجابة 200:
{ "rows": "<tr>...</tr>", "foot": "<tr>...</tr>", "head": null, "count": 50, "total": 230, "next": "50" }foot هو صف الإجماليات عند طلب الإجماليات، وإلا null.
قد تكون total وnext قيمة null. يستبدل وقت التشغيل الصفوف للشبكات ذات الصفحات ويلحقها للتمرير اللانهائي.
ترسل الشبكات المجمّعة وشبكات الشجرة (<table role="treegrid">) path مع POST /_zeyn/grid لتحميل فرع: قيم المجموعات (أو مفاتيح الأسلاف) للصف الذي يُفتح، كما في data-zeyn-path الخاص به. للاستجابة الشكل نفسه؛ وتحوي rows صفوف مجموعات أو صفوفًا أعمق بمستوى واحد، وتعدّها total. يضعها وقت التشغيل بعد الصف المفتوح ويزيل كل صف أعمق يليه عندما يُغلق الصف. والمسار الأطول من مستويات تجميع الشبكة (أو من 32 في الشجرة) يعطي 404.
4.2.1 مزيد من نقاط نهاية الشبكة
| نقطة النهاية | الجسم | تفعل |
|---|---|---|
POST /_zeyn/grid/view | {screen, params, node, view} | تحفظ عرض المستخدم (null يحذفه). 204؛ و404 بلا مخزن عروض |
POST /_zeyn/grid/run | {screen, params, node, key, item} | تشغّل عنصر صف (a0 إجراء صف، m2.1 مسار قائمة). يحمّل الخادم الصف بالمفتاح ويبني الإدخال. {effects} |
POST /_zeyn/grid/bulk | {screen, params, node, action, keys, all, query} | تشغّل إجراءً جماعيًا بالمفاتيح المختارة، أو بكل صف يطابق query.filters عندما تكون all. {effects, count} |
POST /_zeyn/grid/edit | {screen, params, node, key, values, view} | تحفظ تحريرًا مضمّنًا؛ تحمل values الأعمدة المحرَّرة وإصدار الصف كما يرسلها النموذج. {row}؛ و403 للأعمدة غير القابلة للتحرير، و409، و422 |
GET /_zeyn/grid/export | ?screen¶ms&node&q={sort,filters,columns} | تبثّ كل صف مطابق بصيغة CSV |
POST /_zeyn/menu | {screen, params, item} | تشغّل عنصر action من قائمة سياق الشاشة |
4.2.2 التمرير الافتراضي
الشبكات ذات data-zeyn-paging="virtual" تعرض قائمة واحدة قابلة للتمرير على كل الصفوف مهما بلغ عددها. يرسم الخادم الكتلة الأولى (data-zeyn-page-size صفًّا، الإزاحة 0) مع data-zeyn-total: أول 64 صفًّا في الجدول والباقي في <template data-zeyn-rows-later> بعده، أو كل الصفوف في القالب عندما تعرض الشبكة 40 عمودًا أو أكثر. وللشبكة على جذرها أيضًا data-zeyn-row-height (بالبكسل) ومتغير CSS --z-row-height.
وقت التشغيل:
- يضع جسم الجدول في حاوية تمرير ارتفاع فاصلها الداخلي
total * rowHeightبكسل، بحد أقصى 8,000,000 بكسل. وفوق الحد، يُحوَّل موضع التمرير إلى فهرس صف بالنسبة (row = scrollTop / (spacerHeight - viewport) * (total - visibleRows))، فيبلغ 1,000,000 صف نهايته أيضًا. - لا يُبقي في DOM إلا الصفوف المرئية مع 20 صفًّا فوقها وتحتها، موضوعة بـ
transform: translateY(...). للصفوف ارتفاع ثابت؛ ويُقطع النص الطويل بنقاط الحذف. - يحمّل كتلًا من
data-zeyn-page-sizeصفًّا بـPOST /_zeyn/gridوpage: { "type": "offset", "offset": blockStart, "limit": blockSize }، وwith_total: falseبعد الاستدعاء الأول. يحتفظ بما يصل إلى 20 كتلة و100,000 خلية على الأكثر في ذاكرة مؤقتة (تخرج الأقدم استخدامًا أولًا؛ 7 كتل لشبكة من 63 عمودًا، ولا أقل من 3 أبدًا)، وتُحسب الكتل المحفوظة لـreloadضمن الحد نفسه وتُسقَط أولًا، ويجلب مسبقًا الكتلة التالية في اتجاه التمرير، ويلغي طلبات الكتل التي غادرت العرض (AbortController). - لا يمسح الصفوف المرئية أبدًا أثناء تحميل كتلة. تعرض الصفوف المفقودة صف عنصر نائب مع
aria-busy="true"حتى تصل كتلتها، فلا تومض الشاشة. - تغيير الفرز أو المرشّحات يُسقط الذاكرة المؤقتة ويمرّر إلى الأعلى.
- لوحة المفاتيح: مفاتيح الأسهم وPage Up وPage Down وHome وEnd تحرّك الصف النشط وتمرّره إلى العرض. وللجدول
aria-rowcount= total ولكل صفaria-rowindex. - يستخدم نص الموضع (
data-zeyn-page-info) القيمةdata-zeyn-text-range-total.
4.3 إرسال النموذج
POST /_zeyn/form
{ "screen": "product_form", "params": { "id": 7 }, "node": 0, "values": { "code": "P-001", "price": "1.234,56", "active": true } }تُرسل القيم كما كتبها المستخدم: سلاسل لحقول الإدخال والقوائم المنسدلة، وقيم منطقية لخانات الاختيار. ويحلّلها الخادم بحسب نوع الحقل.
يبدأ الخادم من النموذج (model) الذي حمّلته الشاشة ويكتب فوق الحقول الموجودة في النموذج وغير المخصصة للقراءة فقط. أما القيم الأخرى القادمة من العميل فتُتجاهل. ولا تأتي المفاتيح من العميل أبدًا.
الاستجابات:
200 { "effects": [ ... ] }: تم الحفظ؛ نفّذ الـ effects بالترتيب.422 { "errors": [ { "field": "code", "rule": { "rule": "required" }, "message": "Code is required." } ] }: اعرض كل رسالة تحت حقلها (#{scope}-{field}-error)، واضبطaria-invalid="true"على حقل الإدخال، وركّز على أول حقل إدخال غير صالح. وتُعرض الأخطاء ذاتfieldالفارغ أعلى النموذج.
4.4 الأخطاء
قد تجيب أي نقطة نهاية بـ:
| الحالة | الجسم | المعنى |
|---|---|---|
403 | { "error": "forbidden", "message": "..." } | صلاحية مفقودة، أو رفض من حارس (guard)، أو رمز CSRF سيئ |
404 | { "error": "not_found", "message": "..." } | شاشة أو عقدة غير معروفة |
409 | { "error": "conflict", "message": "..." } | غيّر شخص آخر السجل |
422 | انظر 4.3 | التحقق |
504 | { "error": "timeout", "message": "..." } | استغرق الاستدعاء أكثر من حد الخادم |
500 | { "error": "internal", "message": "An unexpected error occurred." } | التفاصيل في سجل الخادم فقط |
يعرض وقت التشغيل message في إشعار (toast) خطأ.
4.5 اللغة
يخزّن POST /_zeyn/locale مع { "locale": "tr" } الاختيار في ملف تعريف الارتباط zeyn_locale ويجيب بـ 204. ثم يعيد وقت التشغيل تحميل الصفحة. وبلا ملف تعريف ارتباط يختار الخادم من Accept-Language، ثم من الافتراضي في التطبيق. و<html lang> يطابق دائمًا اللغة المختارة.
يرسم الهيكل (shell) محدد لغة (select[data-zeyn-locale]) عندما يكون للتطبيق أكثر من لغة واحدة.
4.6 السمة (Theme)
يرسم الهيكل محدد سمة (select[data-zeyn-theme-picker]) إلا إذا أخفاه التطبيق أو قدّم سمة واحدة. يحمل كل خيار data-scheme (light وdark، وفارغ لـ System) وسمات التطبيق data-custom. عند التغيير يضبط وقت التشغيل data-theme وdata-zeyn-theme على <html> ووسم color-scheme meta دفعة واحدة، ثم يرسل POST /_zeyn/theme مع { "theme": "dark" }. ويجيب الخادم بـ 204 مع ملف تعريف الارتباط zeyn_theme، أو 404 لسمة لا يقدمها التطبيق. تُرسم الصفحات بالسمة الموجودة في ملف تعريف الارتباط ذاك، فلا إعادة تحميل ولا وميض. انظر docs/theming.md.
4.7 نقاط نهاية المؤسسات
يُحمَّل enterprise.js وenterprise.css بجانب وقت التشغيل. التفاصيل في docs/enterprise.md.
| نقطة النهاية | الغرض |
|---|---|
POST /_zeyn/reveal {screen, params, node, field, key?} | القيمة الخام لحقل مقنّع، {value}؛ تحتاج صلاحية رفع القناع، وتُدقَّق دائمًا |
POST /_zeyn/reason {screen, reason} | سبب لشاشة معلَّمة بـ reason؛ 204 مع ملف تعريف ارتباط HttpOnly |
POST /_zeyn/import {screen, params, node, csv, commit} | معاينة CSV (commit: false) أو استيراد |
POST /_zeyn/keepalive | "ابقَ مسجّلًا" بعد تحذير الخمول؛ 204 |
GET /_zeyn/health وGET /_zeyn/ready | الحيوية والجاهزية؛ لا يخضع health لحد المعدل، وready فقط من probe_ips |
GET /_zeyn/jobs | حالة المهام للمستخدمين الذين يملكون صلاحية المهام |
تحمل كل استجابة x-zeyn-build. والرفض الذي يتصرف وقت التشغيل بناءً عليه: 428 reason_required (يطلب سببًا ويكرر الطلب) و401 reauth_required (يطلب من المستخدم تسجيل الدخول مجددًا ويكرر الطلب).
5. الأمان
- تضبط كل استجابة
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; img-src 'self' data: blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'وX-Content-Type-Options: nosniffوReferrer-Policy: same-origin. تضيفApp::cspمضيفاتhttpsمسمّاة إلىimg-srcوmedia-src(وblob:) وframe-srcوconnect-srcوfont-src؛ أما التوجيهات الأخرى فلا تتغير أبدًا. ويُبنى نص الترويسة مرة واحدة عند بدء التشغيل. - لا سكربتات مضمّنة ولا معالجات أحداث مضمّنة. تُستخدم سمات
styleالمضمّنة لخصائص CSS المخصصة (--z-widthو--z-flex) ولعروض الأعمدة الثابتة فقط، ولذلك تضيف السياسةstyle-src-attr 'unsafe-inline'. أما السكربتات فتبقى صارمة. - يُخزَّن رمز CSRF في ملف تعريف ارتباط
HttpOnlyوSameSite=Strictويُقارن في زمن ثابت مع ترويسةx-zeyn-csrf. - لا تحمل الصفحات ونموذج
totpوGET /_zeyn/csrfالرمز كما هو في ملف تعريف الارتباط: تحصل كل استجابة على وسادة عشوائية جديدة يليها الرمز XOR الوسادة (128 رقمًا سداسيًّا عشريًّا)، فلا يستطيع gzip تسريبه (BREACH). يزيل الخادم القناع قبل المقارنة ولا يزال يقبل الرمز العادي ذا 64 رقمًا. - الاستجابات الديناميكية بحجم 1 KB أو أكثر (الصفحات والشاشات الجزئية وكتل الشبكات والتصديرات) تُضغط بـ gzip عند المستوى 2 إذا قبله المتصفح، مع
Vary: Accept-Encoding. ولا تُضغط أبدًا: الاستجابات التي لهاContent-Encodingأصلًا (أصول الإطار، تُضغط مرة واحدة وتُخزَّن مؤقتًا)، والنطاقات (Content-Range، وأي طلب فيهRange)، والفيديو والصوت، والصور عدا SVG، وapplication/octet-stream، والأرشيفات وملفات PDF، وtext/event-stream، وapplication/x-ndjson(بث المساعد)، وترقيات WebSocket. - يفحص كل استدعاء، بالترتيب: المصادقة، ثم صلاحية الشاشة، ثم حراس الميزة (guards)، ثم صلاحية action العقدة، ثم يشغّل المعترضات (interceptors) والـ action.
6. سلوك وقت التشغيل
- بلا JavaScript، ما زالت الصفحات تُرسم مع الصفحة الأولى من كل شبكة. أما النماذج والترقيم فتحتاج إلى وقت التشغيل.
- لوحة المفاتيح: تتنقل مفاتيح الأسهم بين صفوف الشبكة، ويفتح Enter صفًّا، ويغلق Escape أعلى مربع حوار، وتعمل الاختصارات من
aria-keyshortcutsفي كل مكان إلا داخل حقول إدخال النص. في الشبكات: يحدّد Space الصف، ويحرّره F2، ويتراجع Ctrl+Z عن آخر تحرير، ويفتح Shift+F10 أو مفتاح قائمة السياق قائمة الصف أو الترويسة، وتفرز Shift+نقرة بعدة أعمدة، ويفتح Right وLeft (معكوسان في الصفحات من اليمين إلى اليسار) تفصيل الصف أو مجموعته أو أبنائه ويغلقانه، ويذهب Left على صف ابن إلى أبيه. يدخل Right على صف إلى خلاياه: تحرّك الأسهم وHome وEnd وCtrl+Home وCtrl+End وTab وShift+Tab مؤشر الخلية، وتوسّع Shift نطاقًا (aria-selectedعلى الخلايا)، وينسخ Ctrl+C النطاق نصًّا مفصولًا بعلامات جدولة ويلصق Ctrl+V في الخلايا القابلة للتحرير، بـPOST /_zeyn/grid/editواحد لكل صف تغيّر. والشبكات ذات 40 عمودًا أو أكثر لا تُبقي في الصفحة إلا الأعمدة المرئية (colgroupبعروض ثابتة، وفواصلtd.z-col-gapمعcolspan، وaria-colindexعلى الخلايا). - النقر الأيمن يفتح قائمة Zeyn حيث تُعرَّف واحدة (الترويسات، والصفوف ذات
data-zeyn-menu، والشاشات ذاتdata-zeyn-menu)؛ وفي أي مكان آخر تبقى قائمة المتصفح نفسه. - ينتقل التركيز إلى مربع الحوار عند فتحه ويعود إلى العنصر الذي فتحه عند إغلاقه.
- تعمل الفحوص من جهة العميل (
requiredوminlengthوmaxlengthوminوmaxوtype="email") قبل الإرسال بواجهة التحقق من القيود في المتصفح. ويتحقق الخادم دائمًا مرة أخرى.
7. مكتبة المكوّنات
تُقدَّم مكوّنات zeyn::ui بأصلين إضافيين يُربطان في رأس الصفحة بعد وقت التشغيل: /_zeyn/components.js و/_zeyn/components.css. وكوقت التشغيل، لا يحتاجان إلى تسجيل دخول.
7.1 نقاط النهاية
كلها POST مع JSON وترويسة CSRF، وتفحص صلاحية الشاشة وحراس الميزة أولًا.
| نقطة النهاية | الجسم | الاستجابة |
|---|---|---|
/_zeyn/node | { screen, params, node } | HTML لعقدة واحدة مع الصفحة الأولى من شبكاتها. تُستخدم في التبويبات الكسولة. |
/_zeyn/data | { screen, params, node, key, input } | JSON من مصدر البيانات key للعقدة: خيارات البحث (lookup) (key هو اسم الحقل)، وأبناء الشجرة (key فارغ)، وفحوص الخطوات (key هو _step). تُفحص صلاحية action المصدر وتعمل المعترضات، كما في الشبكات. والإدخال الذي لا يناسب الـ action يُرفض بـ 404. |
/_zeyn/parse | { kind, text, label } | كيف يقرأ الخادم تاريخًا أو وقتًا مكتوبًا بلغة المستخدم ومنطقته وساعته: { ok, text, iso, utc, zone } أو { ok: false, message }. وkind هو date أو time أو datetime أو local أو presets (اليوم والنطاقات السريعة). |
7.2 القيم التي ترسلها النماذج
- ترسل حقول البحث (Lookups) المعرّفات نصًّا:
"7"، أو"3,7,9"لعدة قيم. - ترسل الأسطر (Lines)
"{field}": "rows"وقيمة واحدة لكل خلية بصيغة"{field}.{line}.{column}"، إضافةً إلى"{field}.{line}._i"، وهو فهرس السطر المحمَّل الذي جاء منه الصف (فارغ للأسطر الجديدة). يعيد الخادم بناء كل سطر من سطره المحمَّل، فلا تتغير إلا الأعمدة المعروضة والقابلة للتحرير. - تستخدم أخطاء التحقق في سطر
{ "field": "lines", "rule": { "rule": "line", "value": { "row": 0, "field": "quantity" } } }.
7.3 الأحداث
| الحدث | على | التفاصيل |
|---|---|---|
zeyn:content | document | عنصر بمحتوى جديد (التنقل، واللوحات الكسولة). يهيّئ كلا السكربتين ما يحويه. |
zeyn:errors | النموذج | أخطاء 422، بعد أن عرضها وقت التشغيل. يستخدمه محرّر الأسطر والنماذج ذات الخطوات. |
zeyn:effect | document | effect لا يعرفه وقت التشغيل: { effect, source, run }. تشغّل run(next) effect آخر. يستخدمه confirm. |
يسأل الـ effect {"effect":"confirm","message":"...","title":"...","ok":"...","cancel":"...","then":{...}} في مربع حوار تنبيه مشروط ولا يشغّل then إلا بعد أن يؤكد المستخدم.
7.4 الوسائط
يُقدَّم Video وAudio بـ /_zeyn/media.js و/_zeyn/media.css، المربوطين بعد مكتبة المكوّنات. انظر docs/media.md.
| نقطة النهاية | تفعل |
|---|---|
GET /_zeyn/media/{source}/{id} | ملف من مصدر وسائط مسجَّل مع طلبات النطاق: 200 و206 و304 و416. |
GET /_zeyn/media/position?source=&id= | { at, kept }، حيث توقف المستخدم المسجّل؛ وkept هي false عندما لا يحتفظ المصدر بمواضع (فيستخدم المشغّل التخزين المحلي). |
POST /_zeyn/media/position | { source, id, at } مع ترويسة CSRF يحفظه: 204، و422 لموضع سيئ. |
7.5 الجزر (Islands)
مع ميزة islands، تُقدَّم الشاشة التي تحوي div[data-zeyn-island] مع <meta name="zeyn-islands"> (اسم الجزيرة إلى عناوين { js, wasm }) و/_zeyn/islands.js وسياسة فيها 'wasm-unsafe-eval'؛ أما الصفحات الأخرى فلا تتغير. والاستجابة الجزئية التي تحوي جزيرة تحمل x-zeyn-islands: 1، ويحمّلها nav.js كاملة عندما يخلو المستند من سياسة الجزر.
| السمة أو نقطة النهاية | المعنى |
|---|---|
data-zeyn-island وdata-zeyn-load وdata-zeyn-props | اسم الجزيرة، وvisible أو interaction أو idle، وJSON الخصائص |
data-zeyn-island-fallback | محتوى بديل، يُخفى متى رُكّبت الجزيرة |
data-zeyn-island-state | يضبطه المحمِّل: loading أو ready أو failed |
GET /_zeyn/islands/{file} | ملف مبني مدرج في البيان (manifest)، مخزَّن مؤقتًا لسنة |
POST /_zeyn/data مع key = اسم بعيد | استدعاء مُنمَّط من الجزيرة، مع كل الفحوص الواردة في 7.1 |
انظر docs/islands.md.
7.6 الزمن الفعلي (Realtime)
مع ميزة realtime، يعلّم div[data-zeyn-live="topic"] العقد التي تتحدّث عندما يُخطر الخادم الموضوع (topic). وتربط الصفحات التي تحوي واحدة /_zeyn/live.js؛ والاستجابة الجزئية التي تحوي واحدة تحمل x-zeyn-live: 1 ويستورد nav.js السكربت. ويعيد zeyn:reload على عنصر تحميل الشبكات داخله في مكانها.
| نقطة النهاية | تفعل |
|---|---|
GET /_zeyn/live/topics?t=a,b&csrf= | WebSocket؛ إطارات نصية { "topic": "a" } |
GET /_zeyn/live/doc/{kind}/{doc}?csrf= | WebSocket؛ إطارات ثنائية 0x01 + تحديث Y (الإصدار 1) في الاتجاهين |
تفحص المصافحات (handshakes) الأصل (origin) ورمز CSRF مقابل ملف تعريف الارتباط والمستخدم المسجّل والصلاحيات قبل الترقية. انظر docs/realtime.md.
يُطلق المشغّل الحدث القابل للإلغاء zeyn:media-adaptive ({ media, kind, url, hls, dash }) على جذره قبل الرجوع إلى الملفات التدريجية؛ وتستدعي مكتبة البث preventDefault() لتتولى الأمر.
8. هيكل بطراز سطح المكتب
يُحمَّل /_zeyn/pro.js و/_zeyn/pro.css بعد مكتبة المكوّنات. وكوقت التشغيل، لا يحتاجان إلى تسجيل دخول.
8.1 الأوامر واللوحة (palette)
تُعلن الأوامر في Rust (Command::open وCommand::navigate وCommand::effect وCommand::action) وتُسجَّل بـ Feature::command. يرسم الهيكل زرًّا مخفيًّا لكل أمر يحق للمستخدم تشغيله (تمر حراس الميزة وتسمح به صلاحية الأمر) داخل div[data-zeyn-commands]:
| السمة | المعنى |
|---|---|
data-zeyn-command | معرّف الأمر |
data-zeyn-effect | effect لأمر من نوع effect، ينفّذه وقت التشغيل عند النقر |
data-zeyn-command-run | معرّف أمر من نوع action، يُشغَّل بـ POST /_zeyn/command |
aria-keyshortcuts | الاختصار بعد التطبيع (Control+Alt+N)؛ ويقوم معالج الاختصارات في وقت التشغيل بالنقر على الزر |
data-hint وdata-icon | الاختصار كما يُعرض للمستخدمين، واسم الأيقونة |
data-text-* على الحاوية | نصوص اللوحة المحلّية |
ترفض App::check الاختصارات التي لا يمكن قراءتها، واختصارات الحرف الواحد (WCAG 2.1.4)، واختصار اللوحة نفسه، والاختصارات أو المعرّفات التي يستخدمها أمران، والاختصارات التي لا يمرّرها المتصفح إلى الصفحة أبدًا (BROWSER_RESERVED: Ctrl+N وCtrl+T وCtrl+W وCtrl+Tab وAlt+F4 وما يقابلها في macOS) و، ما لم يستدعِ الأمر .override_browser()، الاختصارات التي يستخدمها المتصفح (BROWSER_DEFAULTS: الطباعة والحفظ والبحث وشريط العنوان وإعادة التحميل والتكبير وأدوات المطوّر والنسخ واللصق وF1 إلى F12). وتتبع أزرار شريط الأدوات القواعد نفسها وقت التشغيل: فالاختصار من هذا النوع لا يُربط ويُسجَّل السبب. والتركيبات الحرة هي Alt+Shift+<letter> وCtrl+Alt+<letter>.
يفتح Alt+Shift+P (Option+Shift+P في macOS) اللوحة افتراضيًا؛ وتغيّره App::palette_shortcut("Ctrl+Shift+P")، حتى إلى مفتاح يستخدمه المتصفح، عن قصد. الاختصار في data-palette-keys، وتُطابَق الأحرف بالمفتاح الفعلي (physical key) فلا يعطّله Option أو AltGr. وتفتح اللوحة أيضًا من button[data-zeyn-palette-open] في القائمة. وهي <dialog> مشروط مع ARIA combobox وlistbox. تسرد أوامر التطبيق وأزرار شريط الأدوات وأوامر شريط القوائم وعناصر قائمة السياق للشاشة الحالية (النافذة العليا عند فتح واحدة) والشاشات الموجودة في القائمة. المطابقة تقريبية وتتجاهل حالة الأحرف والتشكيل؛ وتأتي الأوامر الثمانية الأحدث تشغيلًا أولًا (مفتاح localStorage هو zeyn.palette.recent). ويغلقها Escape ويعود التركيز إلى موضعه السابق.
| نقطة النهاية | الجسم | الاستجابة |
|---|---|---|
POST /_zeyn/command | { command } | تشغّل أمر action بإدخال ثابت على الخادم: CSRF وحراس الميزة وصلاحية الـ action والمعترضات. { effects }؛ و404 للمعرّفات المجهولة وأوامر effect |
POST /_zeyn/search | { query } | تشغّل كل Feature::search يحق للمستخدم تشغيلها (CSRF وحراس الميزة وصلاحية الـ action والمعترضات) مع قصّ الاستعلام عند 100 حرف. { hits: [{ label, group, url }] }، 20 نتيجة على الأكثر، ومسارات التطبيق فقط |
عندما يكون للحاوية data-search، ترسل اللوحة الاستعلام بعد 150 ms من آخر مفتاح (حرفان أو أكثر) وتسرد النتائج تحت الأوامر. ولا يُرسم إلا الرد على آخر استعلام. واختيار نتيجة يرسل zeyn:navigate مع المسار الأساسي أمام url؛ وتمرّر #anchor فيه إلى ذلك العنصر بعد تبديل الشاشة.
يستمع وقت التشغيل إلى zeyn:run على document لتنفّذ السكربتات الأخرى عملها عبره: { effects, source } يشغّل effects، و{ screen, item } يشغّل عنصر قائمة سياق الشاشة، و{ error } يعرض إشعار (toast) خطأ.
8.2 شريط القوائم وصندوق الأدوات ومفتش الخصائص وشريط الحالة
| الشيفرة | السلوك |
|---|---|
ul[role=menubar][data-zeyn-menubar] | شريط قوائم بحسب W3C APG. العناصر العليا هي button[role=menuitem][aria-haspopup=menu] مع نقطة توقف تبويب متنقلة؛ ويتحكم كل منها بـ ul[role=menu] يكون hidden وهو مغلق. تحمل العناصر سمات الأوامر في 8.1. يتنقل Left وRight بين القوائم، ويفتح Down وEnter وSpace، ويفتح Up عند العنصر الأخير، ويقفز Home وEnd، ويتحرك حرف إلى العنصر التالي الذي يبدأ به، ويغلق Escape ويعود إلى الشريط، ويغلق Tab والنقر خارجها. والقوائم والعناصر التي لا يحق للمستخدم تشغيلها لا تُرسم. |
div[role=toolbar][aria-orientation=vertical][data-zeyn-toolbox] | مجموعة تبديل من button.z-tool[aria-pressed]. تحرّك الأسهم وHome وEnd نقطة توقف التبويب المتنقلة؛ والنقر أو Enter أو Space يشغّل أداة، ويرسل zeyn:tool (detail.tool) ويكتب data-text-selected في شريط الحالة. |
section.z-inspector | نموذج مع مجموعات Form::section (details.z-form-section بأجسام role=group) يُحفظ بـ POST /_zeyn/form عند مسار عقدة المفتش. وتُفتح الأقسام التي تحوي حقولًا فاشلة قبل فحوص المتصفح وبعد 422. |
div[data-zeyn-statusbar] | عناصر عند البداية والنهاية و[data-zeyn-status-live] (role=status وaria-live=polite). ويستبدل zeyn:status (detail.text) الصادر من أي عنصر في الشاشة النص الحي. |
8.3 اللوحات المُرسّاة (Docking)
div.z-dock[data-zeyn-dock] شبكة من أربع section.z-dock-area[data-area] (left وcenter وbottom وright) وثلاثة فواصل div.z-dock-bar[role=separator][data-edge]. الأحجام هي الخصائص المخصصة --z-dock-left و--z-dock-right (نسبة مئوية من العرض) و--z-dock-bottom (نسبة مئوية من الارتفاع)، من 10 إلى 60. لكل منطقة role=tablist من أزرار button.z-dock-tab[data-panel] قابلة للسحب، وزر خيارات، ولوحات role=tabpanel تحوي العقد الأبناء (المسارات كما في أي حاوية؛ ولا تتغير أبدًا عند نقل لوحة). والمناطق الجانبية التي لا لوحات مفتوحة فيها تُخفى.
لوحة المفاتيح: Left وRight (معكوسان في الصفحات من اليمين إلى اليسار) وHome وEnd تتنقل بين علامات التبويب المفتوحة في منطقة؛ ويغلق Delete اللوحة المركَّز عليها؛ ويفتح Shift+F10 أو مفتاح قائمة السياق خيارات المنطقة (النقل إلى منطقة أخرى، والإغلاق، وإظهار اللوحات المغلقة)؛ وتغيّر الأسهم على فاصل الحجم بنسبة 5 بالمئة، ويذهب Home وEnd إلى الحدّين. وإدخال شريط القوائم المصنوع بـ MenuList::panels يسرد كل لوحة بوصفها menuitemcheckbox. وتُعلَن التغييرات في [data-zeyn-dock-live].
| نقطة النهاية | الجسم | الاستجابة |
|---|---|---|
POST /_zeyn/dock | { screen, params, node, save: false } | { layout }: التخطيط المحفوظ للمستخدم المسجّل أو null |
POST /_zeyn/dock | { screen, params, node, save: true, layout } | 204؛ وlayout: null يحذفه |
layout هو { sizes: { left, right, bottom }, panels: [{ id, area, open, active }] } بترتيب علامات التبويب. يفحص الخادم CSRF وصلاحية الشاشة وحراس الميزة، ويبقي لوحات الإرساء نفسها فقط (كل لوحة مرة واحدة) ويقيّد الأحجام، ثم يخزّنه في مخزن العروض في التطبيق (App::datagrid_views) تحت المستأجر والشاشة والعقدة. وبلا مخزن تجيب نقطة النهاية بـ 404 وبلا مستخدم بـ 403؛ ثم يحتفظ المتصفح بالتخطيط في localStorage.
9. وكلاء الذكاء الاصطناعي: MCP وWebMCP
يصل وكلاء الذكاء الاصطناعي الخارجيون إلى التطبيق عبر بابين، كلاهما معطّل افتراضيًا ويُفعَّل بـ App::mcp:
| الإعداد | نقطة نهاية MCP | أدوات WebMCP | الكتابة |
|---|---|---|---|
Mcp::off() (الافتراضي) | 404 | 404 | لا |
Mcp::read_only() | مفعّلة | مفعّلة | لا |
Mcp::on() | مفعّلة | مفعّلة | حفظ النماذج وإجراءات API التي تكتب |
يغلق .server(false) و.webmcp(false) بابًا واحدًا. والمشاريع الجديدة من zeyn new تبدأ بـ Mcp::read_only().
لماذا معطّل: تتيح نقطة النهاية لأي حامل ترويسة Authorization صالحة أن يقود التطبيق عبر نموذج، ويمكن توجيه النموذج بالبيانات التي يقرؤها. يجعل الإطار كل تطبيق جاهزًا للوكلاء (تُولَّد الأدوات من الشيفرة، ولا شيء يُكتب)، لكن فتح التطبيق لهذا الباب، وسماحه للوكلاء بالكتابة، قرار يعود إلى من يشغّلونه. والقراءة فقط على بُعد سطر واحد.
9.1 الأدوات
تُبنى الأدوات من التطبيق للمستخدم المسجّل:
- أداة بحث واحدة لكل شبكة (
{screen}_{node}_search، الإدخال:DataQuery) ومع الكتابة، أداة حفظ واحدة لكل نموذج ({screen}_{node}_save، الإدخال: قيم النموذج كما كُتبت) لكل شاشة ذات مسار تُفتح بلا params؛ - أداة واحدة لكل action في API منشورة (
api_{feature}_{action}). الـ action التي إدخالهاDataQueryتقرأ؛ وكل action أخرى في API تُعدّ كتابة.
يُبنى الوصف من عنوان الشاشة والفقرة الأولى من تعليق التوثيق /// للشاشة وللـ action وإرشادهما ai = "...". أما الشبكات والنماذج التي لا يحق للمستخدم استخدامها، والشاشات والـ actions المعلَّمة بـ ai = false، فلا تُسرد أبدًا وتُرفض عند استدعائها.
يعلّم #[action(confirm)] action لا يجوز للنموذج تشغيلها إلا بعد أن يؤكدها المستخدم في التطبيق. ولا يسرد /_zeyn/mcp مثل هذه الأدوات ولا يشغّلها (لا أحد على تلك القناة يستطيع التأكيد). وتسأل أدوات WebMCP في مربع حوار مشروط (zeyn:confirm) ثم ترسل confirmed: true مع النموذج؛ ويسأل المساعد المدمج في محادثته (انظر docs/assistant.md).
9.2 نقاط النهاية
| نقطة النهاية | تفعل |
|---|---|
GET /_zeyn/tools?screen¶ms | { tools: [{ name, description, inputSchema, kind, screen, node, writes, confirm }], texts } للشاشة المعروضة. يسجّلها zeyn.js في document.modelContext |
POST /_zeyn/mcp | JSON-RPC: initialize وtools/list وtools/call. تحتاج ترويسة Authorization ومستخدمًا مسجّلًا |
POST /_zeyn/form مع via_agent: true | حفظ نموذج عبر WebMCP؛ يحتاج Mcp::on() مع WebMCP. وconfirmed: true فقط بعد أن أكد المستخدم |
يعمل كل استدعاء من وكيل بصفة المستخدم: صلاحية الشاشة والحراس وصلاحية الـ action والمعترضات وحدود المعدل وحد الزمن. وتُدقَّق استدعاءات الوكلاء دائمًا (via_agent: true) وتبقى الحقول المقنّعة مقنّعة بالنسبة لها دائمًا، حتى عندما يحق للمستخدم كشفها. ويعلّم OpenAPI كل عملية بـ x-zeyn-ai: القيمة false لـ ai = false، وإلا { hint, confirm, writes }.