[{"data":1,"prerenderedAt":19344},["ShallowReactive",2],{"writesAr-list":3},[4,1855,6431,10532,12374,14093,18963],{"id":5,"title":6,"body":7,"date":1834,"description":1835,"description_ar":1836,"draft":1837,"extension":1838,"image":1836,"keywords":1839,"lang":1849,"meta":1850,"navigation":324,"path":1851,"seo":1852,"stem":1853,"title_ar":1836,"updatedAt":1836,"__hash__":1854},"writesAr\u002Fwrites-ar\u002Finside-a-skeleton-self-generating-skeleton-loaders.md","داخل @alikhalilll\u002Fa-skeleton، مُحمِّلات skeleton تولّد نفسها بنفسها",{"type":8,"value":9,"toc":1823},"minimark",[10,14,22,25,30,33,39,102,107,138,167,174,188,191,195,202,224,231,242,245,287,290,389,417,423,427,434,479,504,510,513,517,528,944,981,984,1054,1057,1174,1177,1200,1203,1207,1210,1216,1252,1271,1291,1300,1304,1352,1355,1362,1438,1451,1455,1497,1504,1515,1604,1611,1720,1742,1745,1749,1768,1786,1793,1797,1808,1814,1817,1820],[11,12,13],"p",{},"مُحمِّلات الـ skeleton في النهاية واجهة مكرّرة. تبني بطاقة، ثم تبني بطاقة أخرى من مستطيلات رمادية تشبهها، وكلما عدّلت البطاقة الحقيقية نسيتَ أن تعدّل الـ placeholder معها. بعد ثلاثة أسابيع تجد العنوان محاذيًا لليسار في المكوّن الفعلي وموسّطًا في الـ skeleton، ولا ينتبه أحد حتى يمرّ مصمّم بالصفحة على بيئة الـ staging.",[11,15,16,17,21],{},"بنيتُ ",[18,19,20],"code",{},"@alikhalilll\u002Fa-skeleton"," على فرضية مختلفة تمامًا: الـ skeleton يُشتقّ من المكوّن الحقيقي نفسه. تلفّ بطاقتك مرة واحدة فقط، وتتكفّل المكتبة باستنتاج الـ placeholder من قراءة البطاقة. لا قالب موازٍ. لا مستطيلات رمادية تلاحقها بالتحديث. أيّ تعديل تجريه على البطاقة يلتقطه الـ skeleton في العرض التالي دون تدخّل منك.",[11,23,24],{},"ما أتحدّث عنه هنا هو ما يقع تحت السطح: عقد التأليف الذي يجعل هذا الأمر ممكنًا، واستراتيجيات العرض الثلاث وما تدفعه كلٌّ منها من مقايضات، وخط أنابيب الالتقاط خلف وضع clone تحديدًا. هذا الأخير هو ما سأحمله معي إلى أيّ مشكلة مستقبلية من نوع \"أريد نسخة من شكل هذا الشيء الآن\".",[26,27,29],"h2",{"id":28},"العقد-أبقِ-الوسوم-غير-مشروطة-وقيّد-المحتوى-وحده","العقد: أبقِ الوسوم غير مشروطة، وقيّد المحتوى وحده",[11,31,32],{},"قبل أيّ شيء، هناك قاعدة واحدة. هي الأساس الذي تستند إليه كل استراتيجية أذكرها لاحقًا، ولذلك أضعها في الصدارة.",[11,34,35],{},[36,37,38],"strong",{},"خطأ:",[40,41,46],"pre",{"className":42,"code":43,"language":44,"meta":45,"style":45},"language-vue shiki shiki-themes github-light github-dark","\u003Cdiv v-if=\"user\">\n  \u003Ch3>{{ user.name }}\u003C\u002Fh3>\n  \u003Cp>{{ user.bio }}\u003C\u002Fp>\n\u003C\u002Fdiv>\n","vue","",[18,47,48,80,86,92],{"__ignoreMap":45},[49,50,53,57,61,65,68,72,75,77],"span",{"class":51,"line":52},"line",1,[49,54,56],{"class":55},"sVt8B","\u003C",[49,58,60],{"class":59},"s9eBZ","div",[49,62,64],{"class":63},"szBVR"," v-if",[49,66,67],{"class":55},"=",[49,69,71],{"class":70},"sZZnC","\"",[49,73,74],{"class":55},"user",[49,76,71],{"class":70},[49,78,79],{"class":55},">\n",[49,81,83],{"class":51,"line":82},2,[49,84,85],{"class":55},"  \u003Ch3>{{ user.name }}\u003C\u002Fh3>\n",[49,87,89],{"class":51,"line":88},3,[49,90,91],{"class":55},"  \u003Cp>{{ user.bio }}\u003C\u002Fp>\n",[49,93,95,98,100],{"class":51,"line":94},4,[49,96,97],{"class":55},"\u003C\u002F",[49,99,60],{"class":59},[49,101,79],{"class":55},[11,103,104],{},[36,105,106],{},"صحيح:",[40,108,110],{"className":42,"code":109,"language":44,"meta":45,"style":45},"\u003Cdiv>\n  \u003Ch3>{{ user?.name }}\u003C\u002Fh3>\n  \u003Cp>{{ user?.bio }}\u003C\u002Fp>\n\u003C\u002Fdiv>\n",[18,111,112,120,125,130],{"__ignoreMap":45},[49,113,114,116,118],{"class":51,"line":52},[49,115,56],{"class":55},[49,117,60],{"class":59},[49,119,79],{"class":55},[49,121,122],{"class":51,"line":82},[49,123,124],{"class":55},"  \u003Ch3>{{ user?.name }}\u003C\u002Fh3>\n",[49,126,127],{"class":51,"line":88},[49,128,129],{"class":55},"  \u003Cp>{{ user?.bio }}\u003C\u002Fp>\n",[49,131,132,134,136],{"class":51,"line":94},[49,133,97],{"class":55},[49,135,60],{"class":59},[49,137,79],{"class":55},[11,139,140,141,144,145,148,149,151,152,155,156,159,160,159,163,166],{},"الـ walker، سواء كان يقرأ الـ DOM الحيّ أو شجرة الـ vnode، يحتاج أن يرى الشكل ذاته أثناء التحميل وبعد وصول البيانات. لو غلّفتَ العنوان بـ ",[18,142,143],{},"v-if=\"user\"","، فسيرى الـ walker عقدة تعليق أثناء التحميل ثم ",[18,146,147],{},"\u003Ch3>"," بعد ذلك، ولن يستطيع أن يتنبّأ بهندسة الـ placeholder. لكن مع استخدام الاستيفاء الاختياري (optional chaining) يبقى ",[18,150,147],{}," موجودًا دائمًا؛ يكون نصّه فارغًا أثناء التحميل، ويصنّف الـ walker الوسوم الحاملة للنص الفارغة (",[18,153,154],{},"\u003Ch1>","–",[18,157,158],{},"\u003Ch6>",", ",[18,161,162],{},"\u003Cp>",[18,164,165],{},"\u003Cspan>",", وغيرها) على أنها أشرطة نص shimmer بارتفاع العنوان الطبيعي.",[11,168,169,170,173],{},"الوسوم هي المخطط. المحتوى هو الحمولة. تستطيع أن تُبدّل الحمولة بـ ",[18,171,172],{},"null"," دون أن يلاحظ المخطط شيئًا. هذا الانضباط البسيط وحده هو ما يفتح الباب لكل ما يأتي بعده.",[11,175,176,177,180,181,183,184,187],{},"النصف الآخر من العقد هو مخرج الطوارئ ",[18,178,179],{},"data-skeleton-ignore",". لو كان جزء من المكوّن مجرد زخرفة بصرية (خلفية تدرّج، شارة علامة تجارية ثابتة، لمسة SVG) ولا ترغب باستبداله بكتلة رمادية، ضع عليه ",[18,182,179],{}," فيتجاهله الـ walker تمامًا؛ ويُعرض العنصر الحقيقي كما هو في الحالتين. أما ",[18,185,186],{},"data-skeleton-stop"," فيقول للـ walker: \"لا تدخل داخل هذا العنصر؛ اعرضه ككتلة واحدة\". أستخدمه عادةً مع الـ widgets الذرّية حين تكون البنية الداخلية مجرد ضجيج بصري.",[11,189,190],{},"قاعدتان اثنتان وسِمَتان اثنتان. هذا هو كل ما يلزمك تعلّمه من سطح التأليف.",[26,192,194],{"id":193},"وضع-clone-التقاط-الرسم-النهائي-وإعادة-عرض-البكسلات","وضع Clone: التقاط الرسم النهائي وإعادة عرض البكسلات",[11,196,197,198,201],{},"Clone هو الوضع الافتراضي، وهو أهمّ ما في المكتبة برأيي. أغلب مكتبات الـ skeleton تتعثّر لأنها تحاول التفكير على مستوى الـ CSS cascade. تنظر إلى أسماء الفئات. تنظر إلى الأنماط المضمّنة. تفترض قيمة الـ border radius بناءً على بادئة ",[18,199,200],{},"rounded-*",". باختصار، تخمّن.",[11,203,204,205,208,209,212,213,216,217,220,221,223],{},"وضع clone يتخلّى عن التخمين كليًا. يركّب الـ slot خارج الشاشة داخل capture host مضبوط على ",[18,206,207],{},"visibility: hidden","، ويترك المتصفح ينهي كامل الـ layout وحلّ الأنماط، ثم يقرأ ",[36,210,211],{},"النتيجة النهائية المحسوبة"," لكل عنصر عبر ",[18,214,215],{},"getComputedStyle()",". مهما كان نظام التنسيق لديك (أدوات Tailwind، أنماط scoped بأسماء فئات مُجزّأة، CSS-in-JS وقت التشغيل، رموز DaisyUI الدلالية، أو حتى سمات ",[18,218,219],{},"style"," مضمّنة يمرّرها مكوّن أب)، فبحلول اللحظة التي يعمل فيها ",[18,222,215],{}," تكون كلّها قد انهارت إلى إجابة قانونية واحدة لكل خاصية. يأخذ المحرك تلك الإجابة الجاهزة ويعيد عرضها.",[11,225,226,227,230],{},"السطح الذي يلتقطه المحرك شامل فعلًا. حدود مستقلة لكل حافة (فبطاقة لها ",[18,228,229],{},"border-bottom"," مختلف تُقرأ كما ينبغي). أنصاف أقطار مستقلة لكل زاوية (فشكل pill غير المتماثل لا يتراجع إلى مجرد مستطيل مدوّر). لون الخلفية وصورتها. Box-shadow. Opacity. Filter. Transform. Typography. Mix-blend-mode. كل عقدة ملتقطة تحمل حمولة أنماط مضمّنة مجمّدة خاصة بها؛ حلقة العرض لا تخصّص شيئًا لأيّ عقدة، بل تقرأ ما هو مجهّز مسبقًا.",[11,232,233,234,237,238,241],{},"النص هو أكثر ما يخطئ فيه الآخرون، وأكثر ما تصيبه ",[18,235,236],{},"a-skeleton"," بدقّة. بدلًا من تخمين نقاط الالتفاف (\"لنفترض أن النص يلتف كل N حرفًا، ولنفترض أن السطر الأخير عرضه 70%\")، يستخدم المحرك ",[18,239,240],{},"Range.getClientRects()"," ليلتقط المستطيل الفعلي المعروض لكل سطر نصّي بدقّة: left, top, width, height. عنوان موسّط من ثلاثة أسطر على سطح المكتب يصبح عنوانًا موسّطًا من سطرين على الجوّال، وتتقلّص المستطيلات الملتقطة معه. مواضع السطر الأخير في اتجاه RTL، والنص المضبوط (justified)، وفواصل الأسطر اليدوية، كلّها تُعاد بنسبة 1:1 لأنها قِيست فعلًا ولم تُخمَّن.",[11,243,244],{},"واجهة الـ wrapper لكل هذا هي prop واحدة فقط:",[40,246,248],{"className":42,"code":247,"language":44,"meta":45,"style":45},"\u003CASkeleton :loading=\"loading\">\n  \u003CSomeRichComponent \u002F>\n\u003C\u002FASkeleton>\n",[18,249,250,274,279],{"__ignoreMap":45},[49,251,252,254,257,260,264,266,268,270,272],{"class":51,"line":52},[49,253,56],{"class":55},[49,255,256],{"class":59},"ASkeleton",[49,258,259],{"class":55}," :",[49,261,263],{"class":262},"sScJk","loading",[49,265,67],{"class":55},[49,267,71],{"class":70},[49,269,263],{"class":55},[49,271,71],{"class":70},[49,273,79],{"class":55},[49,275,276],{"class":51,"line":82},[49,277,278],{"class":55},"  \u003CSomeRichComponent \u002F>\n",[49,280,281,283,285],{"class":51,"line":88},[49,282,97],{"class":55},[49,284,256],{"class":59},[49,286,79],{"class":55},[11,288,289],{},"ولو أردتَ بناء تدفّق التقاط وإعادة عرض خاص بك، فالدالة الأساسية معرّضة كتصدير عام:",[40,291,295],{"className":292,"code":293,"language":294,"meta":45,"style":45},"language-ts shiki shiki-themes github-light github-dark","import { captureSnapshot, type CaptureSnapshot } from '@alikhalilll\u002Fa-skeleton';\n\nconst snap: CaptureSnapshot = captureSnapshot(myRootEl, {\n  maxDepth: 12,\n  maxNodes: 800,\n  minSize: 4,\n});\n","ts",[18,296,297,320,326,350,361,372,383],{"__ignoreMap":45},[49,298,299,302,305,308,311,314,317],{"class":51,"line":52},[49,300,301],{"class":63},"import",[49,303,304],{"class":55}," { captureSnapshot, ",[49,306,307],{"class":63},"type",[49,309,310],{"class":55}," CaptureSnapshot } ",[49,312,313],{"class":63},"from",[49,315,316],{"class":70}," '@alikhalilll\u002Fa-skeleton'",[49,318,319],{"class":55},";\n",[49,321,322],{"class":51,"line":82},[49,323,325],{"emptyLinePlaceholder":324},true,"\n",[49,327,328,331,335,338,341,344,347],{"class":51,"line":88},[49,329,330],{"class":63},"const",[49,332,334],{"class":333},"sj4cs"," snap",[49,336,337],{"class":63},":",[49,339,340],{"class":262}," CaptureSnapshot",[49,342,343],{"class":63}," =",[49,345,346],{"class":262}," captureSnapshot",[49,348,349],{"class":55},"(myRootEl, {\n",[49,351,352,355,358],{"class":51,"line":94},[49,353,354],{"class":55},"  maxDepth: ",[49,356,357],{"class":333},"12",[49,359,360],{"class":55},",\n",[49,362,364,367,370],{"class":51,"line":363},5,[49,365,366],{"class":55},"  maxNodes: ",[49,368,369],{"class":333},"800",[49,371,360],{"class":55},[49,373,375,378,381],{"class":51,"line":374},6,[49,376,377],{"class":55},"  minSize: ",[49,379,380],{"class":333},"4",[49,382,360],{"class":55},[49,384,386],{"class":51,"line":385},7,[49,387,388],{"class":55},"});\n",[11,390,391,394,395,394,398,401,402,405,406,409,410,413,414,416],{},[18,392,393],{},"maxDepth"," و",[18,396,397],{},"maxNodes",[18,399,400],{},"minSize"," هي حدود الميزانية. بفضلها لا يتوقف الخيط الرئيسي عند جدول من 5000 صفّ. عندما يبلغ الـ walk الحدّ يتوقّف، ويضبط ",[18,403,404],{},"truncated: true","، ثم يسجّل الـ wrapper ",[18,407,408],{},"console.warn"," لمرة واحدة لكل ",[18,411,412],{},"cacheKey"," كي تنتبه للأمر أثناء التطوير لا في الإنتاج. أما ",[18,415,400],{}," فيُسقط أيّ شيء أصغر من أربعة بكسلات CSS على أيّ محور، وهو ما يحذف الخيوط الرفيعة ونقاط الفراغ التي كانت ستظهر لولا ذلك كقصاصات ملوّنة داخل الـ skeleton.",[11,418,419,420,422],{},"المقايضة التي يدفعها وضع clone هي أنه يعمل على العميل فقط. تحتاج إلى DOM حقيقي، وإلى ",[18,421,215],{},"، وإلى متصفح يقرأ الرسم النهائي. على الخادم يُعرض markup الـ slot الحقيقي كالمعتاد، ولا يبدأ الالتقاط وإعادة العرض إلا بعد الـ hydration. لو احتجتَ إلى placeholder جاهز من الخادم قبل انتهاء الـ hydration، فذلك بالضبط ما وُجد وضع mirror من أجله.",[26,424,426],{"id":425},"وضع-mirror-امشِ-على-الـ-vnodes-وسلّمها-إلى-الخادم","وضع Mirror: امشِ على الـ vnodes وسلّمها إلى الخادم",[11,428,429,430,433],{},"Mirror يبدّل المحرك من أساسه. بدلًا من تركيب الـ slot وقراءة الـ DOM، يمشي على شجرة الـ vnode مباشرةً وقت العرض. لا وصول إلى ",[18,431,432],{},"window",". متوافق مع الـ hydration. قابل للعرض من الخادم.",[40,435,437],{"className":42,"code":436,"language":44,"meta":45,"style":45},"\u003CASkeleton mode=\"mirror\" :loading=\"loading\">\n  \u003CSomeRichComponent \u002F>\n\u003C\u002FASkeleton>\n",[18,438,439,467,471],{"__ignoreMap":45},[49,440,441,443,445,448,450,453,455,457,459,461,463,465],{"class":51,"line":52},[49,442,56],{"class":55},[49,444,256],{"class":59},[49,446,447],{"class":262}," mode",[49,449,67],{"class":55},[49,451,452],{"class":70},"\"mirror\"",[49,454,259],{"class":55},[49,456,263],{"class":262},[49,458,67],{"class":55},[49,460,71],{"class":70},[49,462,263],{"class":55},[49,464,71],{"class":70},[49,466,79],{"class":55},[49,468,469],{"class":51,"line":82},[49,470,278],{"class":55},[49,472,473,475,477],{"class":51,"line":88},[49,474,97],{"class":55},[49,476,256],{"class":59},[49,478,79],{"class":55},[11,480,481,482,485,486,489,490,159,493,159,496,499,500,503],{},"يحافظ الـ walker على وسم كل عنصر وعلى فئته الأصلية. الأوراق الحاملة للنص تتحوّل إلى ",[18,483,484],{},"\u003Cspan class=\"a-skel-text-content\">"," (نص شفاف فوق خلفية skeleton) مع ",[18,487,488],{},"box-decoration-break: clone"," كي يلتفّ الـ shimmer حول عرض النص المعروض بدقّة سطرًا سطرًا. أما الوسوم الذرّية والتفاعلية (",[18,491,492],{},"\u003Cimg>",[18,494,495],{},"\u003Cbutton>",[18,497,498],{},"\u003Csvg>",") فتصبح ",[18,501,502],{},"\u003Cdiv class=\"a-skel-block\">"," بأبعاد مستمدّة من فئتها.",[11,505,506,507,509],{},"مكسب Mirror الأساسي هو دعم الـ SSR. ومقايضته هي الدقّة. لأن الـ walker لا يملك ",[18,508,215],{},"، فهو لا يرى إلا ما هو مؤلَّف صراحةً: أسماء الفئات الثابتة والأنماط المضمّنة. لو كانت هوية مكوّنك تأتي من قاعدة CSS-in-JS مُجزّأة أو من كتلة نمط scoped، فلن يلتقطها الـ mirror كما يفعل clone. لكن في معظم التخطيطات تكفي الفئة الثابتة، وأمان الـ SSR يستحق أكثر من آخر 5% من التطابق البكسلي. أما المكوّنات المعقّدة بصريًا فمكانها الطبيعي هو clone.",[11,511,512],{},"كلا الوضعين يتشاركان خط أنابيب الحركة نفسه، والـ cache نفسه، وقاعدة الـ a11y نفسها. التبديل بينهما لا يكلّفك أكثر من prop واحدة.",[26,514,516],{"id":515},"وضع-structural-أشجار-في-الـ-flow-الطبيعي-محفوظة-بين-الجلسات","وضع Structural: أشجار في الـ flow الطبيعي، محفوظة بين الجلسات",[11,518,519,520,523,524,527],{},"الاستراتيجية الثالثة ألجأ إليها حين لا يكون الـ wrapper هو وحدة التنسيق المناسبة لديّ: حين أريد لشكل ملتقط أن يعيش عبر عدة نُسخ، أو أن يستمرّ بين الجلسات، أو أن يعيد التدفّق مع العنصر الأب عند تغيير حجم الـ viewport بدل تثبيته على إحداثيات مطلقة. لتلك الحالة أستخدم ",[18,521,522],{},"useSkeleton()"," مع ",[18,525,526],{},"\u003CASkeletonLayer>",".",[40,529,531],{"className":42,"code":530,"language":44,"meta":45,"style":45},"\u003Cscript setup lang=\"ts\">\nimport { computed, ref } from 'vue';\nimport { useSkeleton } from '@alikhalilll\u002Fa-skeleton';\n\nconst props = defineProps\u003C{ userId: string }>();\nconst user = ref(null);\nconst loading = computed(() => user.value === null);\nconst containerRef = ref\u003CHTMLElement | null>(null);\n\nconst { shape, clear } = useSkeleton({\n  cacheKey: `user-card:${props.userId}`,\n  target: () => (loading.value ? null : containerRef.value),\n  persist: true,\n});\n\nfetchUser(props.userId).then((u) => (user.value = u));\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv ref=\"containerRef\">\n    \u003CASkeletonLayer v-if=\"loading && shape\" :shape=\"shape\" \u002F>\n    \u003CColdStartFallback v-else-if=\"loading\" \u002F>\n    \u003CUserCard v-else :data=\"user\" \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[18,532,533,553,567,580,584,611,631,660,689,694,721,742,766,777,782,787,818,827,832,842,859,886,904,925,935],{"__ignoreMap":45},[49,534,535,537,540,543,546,548,551],{"class":51,"line":52},[49,536,56],{"class":55},[49,538,539],{"class":59},"script",[49,541,542],{"class":262}," setup",[49,544,545],{"class":262}," lang",[49,547,67],{"class":55},[49,549,550],{"class":70},"\"ts\"",[49,552,79],{"class":55},[49,554,555,557,560,562,565],{"class":51,"line":82},[49,556,301],{"class":63},[49,558,559],{"class":55}," { computed, ref } ",[49,561,313],{"class":63},[49,563,564],{"class":70}," 'vue'",[49,566,319],{"class":55},[49,568,569,571,574,576,578],{"class":51,"line":88},[49,570,301],{"class":63},[49,572,573],{"class":55}," { useSkeleton } ",[49,575,313],{"class":63},[49,577,316],{"class":70},[49,579,319],{"class":55},[49,581,582],{"class":51,"line":94},[49,583,325],{"emptyLinePlaceholder":324},[49,585,586,588,591,593,596,599,603,605,608],{"class":51,"line":363},[49,587,330],{"class":63},[49,589,590],{"class":333}," props",[49,592,343],{"class":63},[49,594,595],{"class":262}," defineProps",[49,597,598],{"class":55},"\u003C{ ",[49,600,602],{"class":601},"s4XuR","userId",[49,604,337],{"class":63},[49,606,607],{"class":333}," string",[49,609,610],{"class":55}," }>();\n",[49,612,613,615,618,620,623,626,628],{"class":51,"line":374},[49,614,330],{"class":63},[49,616,617],{"class":333}," user",[49,619,343],{"class":63},[49,621,622],{"class":262}," ref",[49,624,625],{"class":55},"(",[49,627,172],{"class":333},[49,629,630],{"class":55},");\n",[49,632,633,635,638,640,643,646,649,652,655,658],{"class":51,"line":385},[49,634,330],{"class":63},[49,636,637],{"class":333}," loading",[49,639,343],{"class":63},[49,641,642],{"class":262}," computed",[49,644,645],{"class":55},"(() ",[49,647,648],{"class":63},"=>",[49,650,651],{"class":55}," user.value ",[49,653,654],{"class":63},"===",[49,656,657],{"class":333}," null",[49,659,630],{"class":55},[49,661,663,665,668,670,672,674,677,680,682,685,687],{"class":51,"line":662},8,[49,664,330],{"class":63},[49,666,667],{"class":333}," containerRef",[49,669,343],{"class":63},[49,671,622],{"class":262},[49,673,56],{"class":55},[49,675,676],{"class":262},"HTMLElement",[49,678,679],{"class":63}," |",[49,681,657],{"class":333},[49,683,684],{"class":55},">(",[49,686,172],{"class":333},[49,688,630],{"class":55},[49,690,692],{"class":51,"line":691},9,[49,693,325],{"emptyLinePlaceholder":324},[49,695,697,699,702,705,707,710,713,715,718],{"class":51,"line":696},10,[49,698,330],{"class":63},[49,700,701],{"class":55}," { ",[49,703,704],{"class":333},"shape",[49,706,159],{"class":55},[49,708,709],{"class":333},"clear",[49,711,712],{"class":55}," } ",[49,714,67],{"class":63},[49,716,717],{"class":262}," useSkeleton",[49,719,720],{"class":55},"({\n",[49,722,724,727,730,733,735,737,740],{"class":51,"line":723},11,[49,725,726],{"class":55},"  cacheKey: ",[49,728,729],{"class":70},"`user-card:${",[49,731,732],{"class":55},"props",[49,734,527],{"class":70},[49,736,602],{"class":55},[49,738,739],{"class":70},"}`",[49,741,360],{"class":55},[49,743,745,748,751,753,756,759,761,763],{"class":51,"line":744},12,[49,746,747],{"class":262},"  target",[49,749,750],{"class":55},": () ",[49,752,648],{"class":63},[49,754,755],{"class":55}," (loading.value ",[49,757,758],{"class":63},"?",[49,760,657],{"class":333},[49,762,259],{"class":63},[49,764,765],{"class":55}," containerRef.value),\n",[49,767,769,772,775],{"class":51,"line":768},13,[49,770,771],{"class":55},"  persist: ",[49,773,774],{"class":333},"true",[49,776,360],{"class":55},[49,778,780],{"class":51,"line":779},14,[49,781,388],{"class":55},[49,783,785],{"class":51,"line":784},15,[49,786,325],{"emptyLinePlaceholder":324},[49,788,790,793,796,799,802,805,808,810,813,815],{"class":51,"line":789},16,[49,791,792],{"class":262},"fetchUser",[49,794,795],{"class":55},"(props.userId).",[49,797,798],{"class":262},"then",[49,800,801],{"class":55},"((",[49,803,804],{"class":601},"u",[49,806,807],{"class":55},") ",[49,809,648],{"class":63},[49,811,812],{"class":55}," (user.value ",[49,814,67],{"class":63},[49,816,817],{"class":55}," u));\n",[49,819,821,823,825],{"class":51,"line":820},17,[49,822,97],{"class":55},[49,824,539],{"class":59},[49,826,79],{"class":55},[49,828,830],{"class":51,"line":829},18,[49,831,325],{"emptyLinePlaceholder":324},[49,833,835,837,840],{"class":51,"line":834},19,[49,836,56],{"class":55},[49,838,839],{"class":59},"template",[49,841,79],{"class":55},[49,843,845,848,850,852,854,857],{"class":51,"line":844},20,[49,846,847],{"class":55},"  \u003C",[49,849,60],{"class":59},[49,851,622],{"class":262},[49,853,67],{"class":55},[49,855,856],{"class":70},"\"containerRef\"",[49,858,79],{"class":55},[49,860,862,865,868,870,872,875,878,880,883],{"class":51,"line":861},21,[49,863,864],{"class":55},"    \u003C",[49,866,867],{"class":59},"ASkeletonLayer",[49,869,64],{"class":262},[49,871,67],{"class":55},[49,873,874],{"class":70},"\"loading && shape\"",[49,876,877],{"class":262}," :shape",[49,879,67],{"class":55},[49,881,882],{"class":70},"\"shape\"",[49,884,885],{"class":55}," \u002F>\n",[49,887,889,891,894,897,899,902],{"class":51,"line":888},22,[49,890,864],{"class":55},[49,892,893],{"class":59},"ColdStartFallback",[49,895,896],{"class":262}," v-else-if",[49,898,67],{"class":55},[49,900,901],{"class":70},"\"loading\"",[49,903,885],{"class":55},[49,905,907,909,912,915,918,920,923],{"class":51,"line":906},23,[49,908,864],{"class":55},[49,910,911],{"class":59},"UserCard",[49,913,914],{"class":262}," v-else",[49,916,917],{"class":262}," :data",[49,919,67],{"class":55},[49,921,922],{"class":70},"\"user\"",[49,924,885],{"class":55},[49,926,928,931,933],{"class":51,"line":927},24,[49,929,930],{"class":55},"  \u003C\u002F",[49,932,60],{"class":59},[49,934,79],{"class":55},[49,936,938,940,942],{"class":51,"line":937},25,[49,939,97],{"class":55},[49,941,839],{"class":59},[49,943,79],{"class":55},[11,945,946,947,950,951,159,954,159,957,159,960,159,963,159,966,969,970,972,973,976,977,980],{},"يلتقط ",[18,948,949],{},"walkStructural()"," الشكل، وهو walker شجريّ يحافظ على وسوم الحاويات وفئاتها الأصلية وعلى CSS الـ layout المحلول (",[18,952,953],{},"display",[18,955,956],{},"flex-*",[18,958,959],{},"gap",[18,961,962],{},"padding",[18,964,965],{},"grid-*",[18,967,968],{},"box-sizing","). أما الأوراق فتحمل عرضًا وارتفاعًا مضمّنَين إضافةً إلى الإشارات البصرية (background, border, radius). عندما يعيد ",[18,971,526],{}," عرض الشجرة، فإنه يفعل ذلك في ",[36,974,975],{},"الـ flow الطبيعي",": الـ ",[18,978,979],{},"\u003Cdiv class=\"flex flex-col gap-4 p-4\">"," الملتقط يُعرض كحاوية flex فعلية، وكتل الـ shimmer تجلس داخلها كأبناء flex. تغيّر حجم الـ viewport فيعيد الـ layout التدفّق، ولا يتأخّر الـ skeleton خلفه كما يحدث في إعادة عرض clone ذات المواضع المطلقة.",[11,982,983],{},"فلو كان لديك هذا الـ markup الفعلي:",[40,985,989],{"className":986,"code":987,"language":988,"meta":45,"style":45},"language-html shiki shiki-themes github-light github-dark","\u003Cdiv class=\"flex flex-col gap-4 p-4\">\n  \u003Ch3>...\u003C\u002Fh3>\n  \u003Cp>...\u003C\u002Fp>\n  \u003Cbutton>...\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n","html",[18,990,991,1007,1021,1033,1046],{"__ignoreMap":45},[49,992,993,995,997,1000,1002,1005],{"class":51,"line":52},[49,994,56],{"class":55},[49,996,60],{"class":59},[49,998,999],{"class":262}," class",[49,1001,67],{"class":55},[49,1003,1004],{"class":70},"\"flex flex-col gap-4 p-4\"",[49,1006,79],{"class":55},[49,1008,1009,1011,1014,1017,1019],{"class":51,"line":82},[49,1010,847],{"class":55},[49,1012,1013],{"class":59},"h3",[49,1015,1016],{"class":55},">...\u003C\u002F",[49,1018,1013],{"class":59},[49,1020,79],{"class":55},[49,1022,1023,1025,1027,1029,1031],{"class":51,"line":88},[49,1024,847],{"class":55},[49,1026,11],{"class":59},[49,1028,1016],{"class":55},[49,1030,11],{"class":59},[49,1032,79],{"class":55},[49,1034,1035,1037,1040,1042,1044],{"class":51,"line":94},[49,1036,847],{"class":55},[49,1038,1039],{"class":59},"button",[49,1041,1016],{"class":55},[49,1043,1039],{"class":59},[49,1045,79],{"class":55},[49,1047,1048,1050,1052],{"class":51,"line":363},[49,1049,97],{"class":55},[49,1051,60],{"class":59},[49,1053,79],{"class":55},[11,1055,1056],{},"فإن الـ layer يعيد عرضه هكذا:",[40,1058,1060],{"className":986,"code":1059,"language":988,"meta":45,"style":45},"\u003Cdiv\n  class=\"flex flex-col gap-4 p-4\"\n  style=\"display: flex; flex-direction: column; gap: 16px; padding: 16px; box-sizing: border-box\"\n>\n  \u003Cdiv class=\"a-skel\" style=\"width: 200px; height: 24px; ...\" \u002F>\n  \u003Cdiv class=\"a-skel\" style=\"width: 280px; height: 16px; ...\" \u002F>\n  \u003Cdiv class=\"a-skel\" style=\"width: 120px; height: 36px; ...\" \u002F>\n\u003C\u002Fdiv>\n",[18,1061,1062,1069,1079,1089,1093,1120,1143,1166],{"__ignoreMap":45},[49,1063,1064,1066],{"class":51,"line":52},[49,1065,56],{"class":55},[49,1067,1068],{"class":59},"div\n",[49,1070,1071,1074,1076],{"class":51,"line":82},[49,1072,1073],{"class":262},"  class",[49,1075,67],{"class":55},[49,1077,1078],{"class":70},"\"flex flex-col gap-4 p-4\"\n",[49,1080,1081,1084,1086],{"class":51,"line":88},[49,1082,1083],{"class":262},"  style",[49,1085,67],{"class":55},[49,1087,1088],{"class":70},"\"display: flex; flex-direction: column; gap: 16px; padding: 16px; box-sizing: border-box\"\n",[49,1090,1091],{"class":51,"line":94},[49,1092,79],{"class":55},[49,1094,1095,1097,1099,1101,1103,1106,1109,1111,1114,1118],{"class":51,"line":363},[49,1096,847],{"class":55},[49,1098,60],{"class":59},[49,1100,999],{"class":262},[49,1102,67],{"class":55},[49,1104,1105],{"class":70},"\"a-skel\"",[49,1107,1108],{"class":262}," style",[49,1110,67],{"class":55},[49,1112,1113],{"class":70},"\"width: 200px; height: 24px; ...\"",[49,1115,1117],{"class":1116},"s7hpK"," \u002F",[49,1119,79],{"class":55},[49,1121,1122,1124,1126,1128,1130,1132,1134,1136,1139,1141],{"class":51,"line":374},[49,1123,847],{"class":55},[49,1125,60],{"class":59},[49,1127,999],{"class":262},[49,1129,67],{"class":55},[49,1131,1105],{"class":70},[49,1133,1108],{"class":262},[49,1135,67],{"class":55},[49,1137,1138],{"class":70},"\"width: 280px; height: 16px; ...\"",[49,1140,1117],{"class":1116},[49,1142,79],{"class":55},[49,1144,1145,1147,1149,1151,1153,1155,1157,1159,1162,1164],{"class":51,"line":385},[49,1146,847],{"class":55},[49,1148,60],{"class":59},[49,1150,999],{"class":262},[49,1152,67],{"class":55},[49,1154,1105],{"class":70},[49,1156,1108],{"class":262},[49,1158,67],{"class":55},[49,1160,1161],{"class":70},"\"width: 120px; height: 36px; ...\"",[49,1163,1117],{"class":1116},[49,1165,79],{"class":55},[49,1167,1168,1170,1172],{"class":51,"line":662},[49,1169,97],{"class":55},[49,1171,60],{"class":59},[49,1173,79],{"class":55},[11,1175,1176],{},"تبقى الفئة الأصلية موجودة كي يظلّ نظام الـ CSS الـ utility-first ساريًا، ويُضمَّن CSS الـ layout المحلول كنسخة احتياطية كي يبدو الـ skeleton صحيحًا حتى لو لم تُحمَّل ورقة الأنماط عند نقطة التركيب. يهمّ هذا كثيرًا لو كنت تعرض الشكل الملتقط داخل سياق CSS مختلف، مثل نافذة معاينة على هيئة بريد إلكتروني، أو shell معروض من الخادم لا يجلب Tailwind أصلًا.",[11,1178,1179,1180,1183,1184,1187,1188,1191,1192,1195,1196,1199],{},"الميزة الأخرى التي يمنحك إياها وضع structural هي الاستمرارية. تجعل ",[18,1181,1182],{},"persist: true"," الشكل الملتقط ينعكس إلى ",[18,1185,1186],{},"localStorage"," تحت البادئة ",[18,1189,1190],{},"a-skeleton:s:",". عند البدء البارد (cold start) في زيارة عائدة، يقرأ الـ cache الشكل المحفوظ ويعيد عرض الـ skeleton في أول رسم مباشرةً، دون وميض \"shimmer عام\" بينما يُركَّب المكوّن الحقيقي ويقيس نفسه. الـ cache مُصدَّر مخططه (",[18,1193,1194],{},"v: 3"," لـ structural، و",[18,1197,1198],{},"v: 2"," للشكل المسطّح القديم)، ولذلك تُطهَّر الإصدارات غير المتطابقة تلقائيًا عند القراءة. لا يمكنك إعادة عرض هندسة الأمس عبر مُعارض اليوم لو تغيّر شكل البيانات.",[11,1201,1202],{},"الاستراتيجيات الثلاث تختار سلوكيات افتراضية مختلفة انطلاقًا من البدائيات نفسها. Clone يعيد عرض بكسلات بمواضع مطلقة. Mirror يعيد عرض vnodes داخل الشجرة الأصلية. Structural يعيد عرض الحاويات في الـ flow الطبيعي. وحدة cache واحدة، وسطح theming واحد، وقاعدة a11y واحدة، وثلاثة محرّكات مختلفة.",[26,1204,1206],{"id":1205},"خط-أنابيب-الالتقاط-والتخزين-المؤقت-وإعادة-العرض","خط أنابيب الالتقاط والتخزين المؤقت وإعادة العرض",[11,1208,1209],{},"القصة المعمارية واحدة في الأوضاع الثلاثة، حتى وإن تباينت التنفيذات. إليك دورة تحميل واحدة من أوّلها إلى آخرها.",[11,1211,1212,1215],{},[36,1213,1214],{},"الرسم الأول، وفشل الـ cache."," يُركَّب الـ wrapper، ولا يوجد شكل مخزّن بعد. في وضعَي clone وmirror يمشي المحرك على بنية الـ slot (vnodes في mirror، وتركيب DOM خارج الشاشة في clone) وينتج placeholder يعكس بنية وسوم الـ slot. تحتفظ الحاويات بوسومها الدلالية وبـ CSS الـ layout الخاص بها. أما الأوراق الذرّية والعناصر الحاملة للنص فتتحوّل إلى placeholders من نوع shimmer. يرى المستخدم skeleton بنيويًا يطابق حدود المكوّن الحقيقي على الفور، قبل أن تصل أيّ بيانات.",[11,1217,1218,1221,1222,523,1225,1228,1229,1232,1233,1235,1236,1238,1239,1242,1243,1245,1246,1248,1249,1251],{},[36,1219,1220],{},"وصول البيانات."," يُعرض المحتوى الحقيقي داخل الـ slot. يجدول ",[18,1223,1224],{},"ResizeObserver",[18,1226,1227],{},"requestAnimationFrame"," تمريرة قياس. في وضع clone تقرأ ",[18,1230,1231],{},"captureSnapshot()"," قيمة ",[18,1234,215],{}," لكل عنصر ومستطيلات النص لكل سطر عبر ",[18,1237,240],{},"، ثم تجمّد النتيجة داخل شجرة ",[18,1240,1241],{},"CaptureSnapshot",". في وضع structural تنتج ",[18,1244,949],{}," شجرة مجمّدة تحمل فيها الحاويات CSS الـ layout الخاص بها، وتحمل الأوراق أبعادها وإشاراتها البصرية. في الحالتين تصبح اللقطة الملتقطة في الذاكرة الآن (وفي ",[18,1247,1186],{}," أيضًا لو كانت ",[18,1250,1182],{},").",[11,1253,1254,1257,1258,1260,1261,1263,1264,1267,1268,1270],{},[36,1255,1256],{},"التقلّب التالي إلى حالة التحميل."," يعود المستخدم إلى الصفحة، أو يطلق إعادة جلب، أو يزور المسار في جلسة لاحقة. تنقلب ",[18,1259,263],{}," إلى ",[18,1262,774],{},". يُعاد عرض الشكل المخزّن فورًا، بلا خطوة وسيطة من نوع \"خمّن الشكل\". في وضع clone يعرض ",[18,1265,1266],{},"\u003CASkeletonClone>"," شجرة من الـ divs ذات المواضع المطلقة، يحمل كلٌّ منها نمطه المضمّن المجمّد مسبقًا؛ وحلقة العرض خالية تمامًا من التخصيص لأن كل حمولة نمط حُسبت وقت الالتقاط. في وضع structural يعرض ",[18,1269,526],{}," الشجرة في الـ flow الطبيعي، مع تشغيل CSS الـ layout الملتقط لسلوك حاوية flex أو grid.",[11,1272,1273,1274,1276,1277,1279,1280,1282,1283,1285,1286,409,1288,1290],{},"للميزانيات دور في كل خطوة. ",[18,1275,397],{}," (افتراضيًا 500–800 بحسب الـ walker) هو حدّ حجم الشجرة. ",[18,1278,393],{}," (افتراضيًا 12–16) هو حدّ الاستدعاء العودي. ",[18,1281,400],{}," (افتراضيًا 4 بكسلات CSS) هو مرشّح لكل ورقة. هذه الأرقام ليست اعتباطية: اخترتُها بحيث تبقى لوحة تحكم مزدحمة بمئات الأوراق قابلة للمشي داخل rAF واحد، وبحيث يتّسع إدخال ",[18,1284,1186],{}," للشجرة الملتقطة دون تجاوز الحصّة البالغة 5 ميغابايت. وحين يُقتطع الـ walk، يسجّل الـ wrapper ",[18,1287,408],{},[18,1289,412],{}," كي تكتشف الأمر في التطوير، لا في الإنتاج.",[11,1292,1293,1294,394,1297,1299],{},"قراءة layout واحدة في البداية، ثم قيم مخزّنة في بقية المشي. تحدث استدعاءات ",[18,1295,1296],{},"getBoundingClientRect()",[18,1298,215],{}," في تمريرة واحدة من أعلى إلى أسفل، دون كتابات تتخلّلها، ودون قذف تخطيط: reflow قسري واحد، ثم تصبح اللقطة الملتقطة دائمة بعده.",[26,1301,1303],{"id":1302},"البدائيات-الـ-15-المسمّاة","البدائيات الـ 15 المسمّاة",[11,1305,1306,1307,159,1310,159,1313,159,1316,159,1319,159,1322,159,1325,159,1328,159,1331,159,1334,159,1337,159,1340,159,1343,159,1346,159,1349,527],{},"التوليد الذاتي لا يناسب كل حالة. لو لم يكن لحالة التحميل عندك مكوّن حقيقي يستحق أن يُلَفّ (كأن تعرض الـ skeleton قبل بدء أيّ جلب بيانات، أو يكون الـ placeholder حقًا مختلف الـ markup عن العرض النهائي)، فلا يجد الالتقاط التلقائي شيئًا يقرأه أصلًا. لهذه الحالات تشحن الحزمة 15 متغيّرًا مسمّى: ",[18,1308,1309],{},"\u003CASkeletonCard>",[18,1311,1312],{},"\u003CASkeletonText>",[18,1314,1315],{},"\u003CASkeletonHeading>",[18,1317,1318],{},"\u003CASkeletonAvatar>",[18,1320,1321],{},"\u003CASkeletonImage>",[18,1323,1324],{},"\u003CASkeletonVideo>",[18,1326,1327],{},"\u003CASkeletonButton>",[18,1329,1330],{},"\u003CASkeletonInput>",[18,1332,1333],{},"\u003CASkeletonChip>",[18,1335,1336],{},"\u003CASkeletonListItem>",[18,1338,1339],{},"\u003CASkeletonTable>",[18,1341,1342],{},"\u003CASkeletonChart>",[18,1344,1345],{},"\u003CASkeletonForm>",[18,1347,1348],{},"\u003CASkeletonArticle>",[18,1350,1351],{},"\u003CASkeletonDivider>",[11,1353,1354],{},"هذه ليست بديلًا عن مسار الالتقاط التلقائي، بل مخرجًا جانبيًا حين تحتاجه. تشترك في خط أنابيب الحركة نفسه، وفي رموز الـ theming نفسها، وفي قاعدة الـ a11y نفسها. تؤلّفها يدويًا عند الحاجة، وإلا فدع الـ wrapper يقوم بالعمل.",[11,1356,1357,1358,1361],{},"يوجد أيضًا ",[18,1359,1360],{},"\u003CASkeletonBlock>"," للحالة التي لا يناسبها أيّ من المتغيّرات وتريد تركيب نسختك الخاصة:",[40,1363,1365],{"className":42,"code":1364,"language":44,"meta":45,"style":45},"\u003Cdiv v-if=\"loading\" class=\"flex items-start gap-4 p-4\">\n  \u003CASkeletonBlock type=\"circle\" :w=\"64\" :h=\"64\" \u002F>\n  \u003Cdiv class=\"flex-1 space-y-2\">\n    \u003CASkeletonBlock type=\"text\" :w=\"160\" :h=\"18\" \u002F>\n    \u003CASkeletonBlock type=\"text\" :w=\"100\" :h=\"12\" \u002F>\n    \u003CASkeletonBlock type=\"text\" :lines=\"3\" :h=\"14\" class=\"!mt-3\" \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\u003CUserCard v-else :data=\"user\" \u002F>\n",[18,1366,1367,1392,1397,1402,1407,1412,1417,1425,1433],{"__ignoreMap":45},[49,1368,1369,1371,1373,1375,1377,1379,1381,1383,1385,1387,1390],{"class":51,"line":52},[49,1370,56],{"class":55},[49,1372,60],{"class":59},[49,1374,64],{"class":63},[49,1376,67],{"class":55},[49,1378,71],{"class":70},[49,1380,263],{"class":55},[49,1382,71],{"class":70},[49,1384,999],{"class":262},[49,1386,67],{"class":55},[49,1388,1389],{"class":70},"\"flex items-start gap-4 p-4\"",[49,1391,79],{"class":55},[49,1393,1394],{"class":51,"line":82},[49,1395,1396],{"class":55},"  \u003CASkeletonBlock type=\"circle\" :w=\"64\" :h=\"64\" \u002F>\n",[49,1398,1399],{"class":51,"line":88},[49,1400,1401],{"class":55},"  \u003Cdiv class=\"flex-1 space-y-2\">\n",[49,1403,1404],{"class":51,"line":94},[49,1405,1406],{"class":55},"    \u003CASkeletonBlock type=\"text\" :w=\"160\" :h=\"18\" \u002F>\n",[49,1408,1409],{"class":51,"line":363},[49,1410,1411],{"class":55},"    \u003CASkeletonBlock type=\"text\" :w=\"100\" :h=\"12\" \u002F>\n",[49,1413,1414],{"class":51,"line":374},[49,1415,1416],{"class":55},"    \u003CASkeletonBlock type=\"text\" :lines=\"3\" :h=\"14\" class=\"!mt-3\" \u002F>\n",[49,1418,1419,1421,1423],{"class":51,"line":385},[49,1420,930],{"class":55},[49,1422,60],{"class":59},[49,1424,79],{"class":55},[49,1426,1427,1429,1431],{"class":51,"line":662},[49,1428,97],{"class":55},[49,1430,60],{"class":59},[49,1432,79],{"class":55},[49,1434,1435],{"class":51,"line":691},[49,1436,1437],{"class":55},"\u003CUserCard v-else :data=\"user\" \u002F>\n",[11,1439,1440,1442,1443,1446,1447,1450],{},[18,1441,1360],{}," صديق للـ flow. يتركّب بسلاسة مع تخطيطات flex وgrid والـ stack. تكدّس prop ",[18,1444,1445],{},"lines"," على ",[18,1448,1449],{},"type=\"text\""," عددًا N من الأشرطة مع جعل الأخير بعرض 70%، وهو استدلال يكون صحيحًا بدرجة كافية في أغلب الحالات حين لا يتوفّر مكوّن حقيقي تقيسه.",[26,1452,1454],{"id":1453},"الحركات-والـ-theming-بوصفها-استراتيجيات","الحركات والـ theming بوصفها استراتيجيات",[11,1456,1457,1458,1461,1462,1465,1466,1469,1470,1473,1474,1477,1478,1481,1482,1469,1485,1488,1489,1492,1493,1496],{},"تُشحن معنا أربعة أوضاع للحركة: ",[18,1459,1460],{},"shimmer"," (تدرّج ماسح على عنصر زائف ",[18,1463,1464],{},"::after","، محتوى داخل كل كتلة عبر ",[18,1467,1468],{},"overflow: hidden",")، و",[18,1471,1472],{},"pulse"," (تدوير opacity من 1 إلى ",[18,1475,1476],{},"--ak-skel-pulse-min"," والعودة)، و",[18,1479,1480],{},"wave"," (تدرّج منزلق عبر ",[18,1483,1484],{},"background-position",[18,1486,1487],{},"none"," (كتل ثابتة). كلٌّ منها استراتيجية قائمة بذاتها، وليست فرعًا شرطيًا. تمرّر ",[18,1490,1491],{},"animation=\"pulse\""," فتنخرط فئة الـ CSS المناسبة تلقائيًا. لا سلسلة ",[18,1494,1495],{},"if (animation === 'pulse')"," داخل المكوّن.",[11,1498,1499,1500,1503],{},"يعطّل ",[18,1501,1502],{},"prefers-reduced-motion: reduce"," كل حركة تلقائيًا. يهبط عنصر shimmer الزائف إلى طبقة ثابتة بشفافية منخفضة، ويتوقّف الـ pulse عن التدوير. لا شيفرة مكوّن معنيّة بذلك أصلًا؛ الأمر مجرد استعلام media في ورقة الأنماط.",[11,1505,1506,1507,1510,1511,1514],{},"الـ theming يقوم على متغيرات CSS. الرموز منطاقة تحت ",[18,1508,1509],{},"--ak-skel-*"," (مع أسماء بديلة متوافقة رجعيًا تحت ",[18,1512,1513],{},"--ak-skeleton-*"," لمستخدمي الإصدار الأول):",[1516,1517,1518,1531],"table",{},[1519,1520,1521],"thead",{},[1522,1523,1524,1528],"tr",{},[1525,1526,1527],"th",{},"الرمز",[1525,1529,1530],{},"يُستخدم لأجل",[1532,1533,1534,1545,1555,1565,1575,1584,1594],"tbody",{},[1522,1535,1536,1542],{},[1537,1538,1539],"td",{},[18,1540,1541],{},"--ak-skel-base",[1537,1543,1544],{},"تعبئة الكتلة",[1522,1546,1547,1552],{},[1537,1548,1549],{},[18,1550,1551],{},"--ak-skel-highlight",[1537,1553,1554],{},"لون مسح shimmer \u002F wave",[1522,1556,1557,1562],{},[1537,1558,1559],{},[18,1560,1561],{},"--ak-skel-radius",[1537,1563,1564],{},"نصف قطر حدود الكتلة الافتراضي",[1522,1566,1567,1572],{},[1537,1568,1569],{},[18,1570,1571],{},"--ak-skel-duration",[1537,1573,1574],{},"طول دورة الحركة",[1522,1576,1577,1581],{},[1537,1578,1579],{},[18,1580,1476],{},[1537,1582,1583],{},"قيمة opacity عند قاع دورة الـ pulse",[1522,1585,1586,1591],{},[1537,1587,1588],{},[18,1589,1590],{},"--ak-skel-ring",[1537,1592,1593],{},"لون حلقة داخلية خفيفة بعرض 1 بكسل",[1522,1595,1596,1601],{},[1537,1597,1598],{},[18,1599,1600],{},"--ak-skel-icon",[1537,1602,1603],{},"لون أيقونة placeholder (متغيرات image \u002F video)",[11,1605,1606,1607,1610],{},"تستطيع أن تكتب فوق هذه الرموز على ",[18,1608,1609],{},":root","، أو على فئة wrapper لأوضاع multi-tenant، أو مضمّنة على نسخة واحدة بعينها:",[40,1612,1616],{"className":1613,"code":1614,"language":1615,"meta":45,"style":45},"language-css shiki shiki-themes github-light github-dark",".tenant-acme {\n  --ak-skel-base: hsl(220 30% 18%);\n  --ak-skel-highlight: hsl(220 60% 60% \u002F 0.35);\n  --ak-skel-radius: 0.5rem;\n  --ak-skel-duration: 2s;\n}\n","css",[18,1617,1618,1626,1655,1685,1700,1715],{"__ignoreMap":45},[49,1619,1620,1623],{"class":51,"line":52},[49,1621,1622],{"class":262},".tenant-acme",[49,1624,1625],{"class":55}," {\n",[49,1627,1628,1631,1634,1637,1639,1642,1645,1648,1651,1653],{"class":51,"line":82},[49,1629,1630],{"class":601},"  --ak-skel-base",[49,1632,1633],{"class":55},": ",[49,1635,1636],{"class":333},"hsl",[49,1638,625],{"class":55},[49,1640,1641],{"class":333},"220",[49,1643,1644],{"class":333}," 30",[49,1646,1647],{"class":63},"%",[49,1649,1650],{"class":333}," 18",[49,1652,1647],{"class":63},[49,1654,630],{"class":55},[49,1656,1657,1660,1662,1664,1666,1668,1671,1673,1675,1677,1680,1683],{"class":51,"line":88},[49,1658,1659],{"class":601},"  --ak-skel-highlight",[49,1661,1633],{"class":55},[49,1663,1636],{"class":333},[49,1665,625],{"class":55},[49,1667,1641],{"class":333},[49,1669,1670],{"class":333}," 60",[49,1672,1647],{"class":63},[49,1674,1670],{"class":333},[49,1676,1647],{"class":63},[49,1678,1679],{"class":55}," \u002F ",[49,1681,1682],{"class":333},"0.35",[49,1684,630],{"class":55},[49,1686,1687,1690,1692,1695,1698],{"class":51,"line":94},[49,1688,1689],{"class":601},"  --ak-skel-radius",[49,1691,1633],{"class":55},[49,1693,1694],{"class":333},"0.5",[49,1696,1697],{"class":63},"rem",[49,1699,319],{"class":55},[49,1701,1702,1705,1707,1710,1713],{"class":51,"line":363},[49,1703,1704],{"class":601},"  --ak-skel-duration",[49,1706,1633],{"class":55},[49,1708,1709],{"class":333},"2",[49,1711,1712],{"class":63},"s",[49,1714,319],{"class":55},[49,1716,1717],{"class":51,"line":374},[49,1718,1719],{"class":55},"}\n",[11,1721,1722,1723,1726,1727,1730,1731,1734,1735,1737,1738,1741],{},"الوضع الداكن يعمل تلقائيًا عبر محدّدات ",[18,1724,1725],{},":where(.dark)"," التي تعيد تلوين الرموز (فئة ",[18,1728,1729],{},".dark"," الخاصة بـ Tailwind، وفئة shadcn، وفئة ",[18,1732,1733],{},"nuxt-color-mode","، وأيّ شيء يضع سلفًا يحمل ",[18,1736,1729],{}," في الشجرة)، إضافةً إلى نسخة احتياطية عبر ",[18,1739,1740],{},"@media (prefers-color-scheme: dark)"," للحالة التي لا توجد فيها فئة صريحة على الشجرة.",[11,1743,1744],{},"المقصد من هذا التصميم أن لا يضطر مستهلك المكتبة يومًا إلى الكتابة فوق فئة داخلية. كل قرار بصري هو رمز؛ والرموز موروثة عبر الـ cascade؛ والكتابة فوقها لنسخة بعينها تكون بأنماط مضمّنة. المكوّن يملك البنية والحركة، وأنت تملك المظهر.",[26,1746,1748],{"id":1747},"a11y-لا-تحتاج-إلى-تكوينها","a11y لا تحتاج إلى تكوينها",[11,1750,1751,1752,1755,1756,1759,1760,1763,1764,1767],{},"الإعدادات الافتراضية هي ذاتها التي كنتَ ستريدها على أيّ حال. يحمل كل wrapper وكل جذر متغيّر السمة ",[18,1753,1754],{},"role=\"status\""," أثناء التحميل، وسمة ",[18,1757,1758],{},"aria-busy=\"true\""," تعكس حالة التحميل، وسمة ",[18,1761,1762],{},"aria-live=\"polite\""," كي تُعلن قارئات الشاشة حالة التحميل دون مقاطعة المستخدم. يُشحن ",[18,1765,1766],{},"\u003Cspan class=\"a-skel-sr-only\">Loading...\u003C\u002Fspan>"," مخفيّ بصريًا مع كل متغيّر ليكون للإعلان نصّ يقرأه.",[11,1769,1770,1771,1774,1775,394,1778,1781,1782,1785],{},"يحمل كل سطح shimmer مُصدَر السمة ",[18,1772,1773],{},"aria-hidden=\"true\"","، لأن الـ placeholder زخرفة والـ wrapper هو ما يُعلَن. تعطّل skeletons وضع mirror الخصائص ",[18,1776,1777],{},"user-select",[18,1779,1780],{},"pointer-events"," على شجرة الـ slot كي لا يستطيع أحد التفاعل مع الـ placeholder في منتصف التحميل. ويزيل ",[18,1783,1784],{},"prefers-reduced-motion"," الحركة كلها.",[11,1787,1788,1789,1792],{},"لا شيء من هذا قابل للتكوين. ولا ينبغي أن يكون. الإعدادات الافتراضية هي الإجابة الصحيحة. لو احتجتَ إلى إيقافها، فأنت إما تختبر شيئًا محدّدًا (وفي هذه الحالة ",[18,1790,1791],{},"animation=\"none\""," هو المفتاح المناسب)، أو تحاول شيئًا على مستوى الـ a11y لا أرغب أنا في أن أجعله سهلًا.",[26,1794,1796],{"id":1795},"ما-علّمني-إياه-وضع-clone","ما علّمني إياه وضع Clone",[11,1798,1799,1800,1803,1804,1807],{},"الشيء الذي سأحمله معي إلى المشكلة التالية ليس الـ API. بل هي البصيرة التي يقوم عليها وضع clone. تستطيع أن تتوقف عن التفكير على مستوى الـ cascade. تستطيع أن تتوقف عن محاولة التنبّؤ بما تنتجه فعلًا قيمة ",[18,1801,1802],{},"border-radius: 0.75rem"," مخلوطة بـ ",[18,1805,1806],{},"rounded-2xl"," مخلوطة بكتابة فوقية scoped. ركّب الشيء. دع المتصفح يطوي كل المدخلات. اقرأ الإجابة النهائية. أعِد عرض الإجابة النهائية.",[11,1809,1810,1811,1813],{},"هذا المبدأ ينطبق في كل موضع لديك فيه مشكلة \"أحتاج نسخة من هذا العنصر\". معاينات الطباعة. معاينات السحب. معارض المكوّنات التي تعرض الـ widget نفسه في خمسة أوضاع مختلفة. نهج \"انظر إلى ما هو موجود فعلًا على الشاشة، لا إلى القواعد التي أنتجته\" يمتدّ أبعد مما يُعطى من فضل. كل خط أنابيب CSS، وكل متغيّر، وكل أولوية cascade، وكل إطار حركة، ينتهي إلى رقم واحد لكل خاصية لكل عنصر، وذلك الرقم هو ما سيخبرك به ",[18,1812,215],{}," متى ما سألته.",[11,1815,1816],{},"الشيء الآخر الذي أثبت جدواه فعلًا هو عقد التأليف. وسوم غير مشروطة، ومحتوى عبر الاستيفاء. بمجرد أن ترسخ هذه الفكرة في ذهنك، يصبح كل مكوّن تبنيه جاهزًا للـ skeleton تلقائيًا: لا قالب إضافي، ولا حالة موازية، ولا عبء صيانة. المكتبة تقرأ مكوّنك الحقيقي، وأنت تكتبه بالطريقة التي كنتَ تريدها أصلًا.",[11,1818,1819],{},"فكرتان اثنتان، وحزمة واحدة. المستودع مرخّص برخصة MIT، وستجد في الـ README المرجع الطويل لكل prop وslot لو أردتَ التعمّق أكثر. لو رأيتَ شيئًا يمكن أن يكون أكثر حدّة، افتح issue من فضلك. أفضّل بصدق أن أُصحَّح على أن أبقى مرتاحًا.",[219,1821,1822],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":45,"searchDepth":82,"depth":82,"links":1824},[1825,1826,1827,1828,1829,1830,1831,1832,1833],{"id":28,"depth":82,"text":29},{"id":193,"depth":82,"text":194},{"id":425,"depth":82,"text":426},{"id":515,"depth":82,"text":516},{"id":1205,"depth":82,"text":1206},{"id":1302,"depth":82,"text":1303},{"id":1453,"depth":82,"text":1454},{"id":1747,"depth":82,"text":1748},{"id":1795,"depth":82,"text":1796},"2026-06-13","كيف تستخرج a-skeleton واجهة الانتظار من المكوّن نفسه. وضع Clone، ووضع Structural، ووضع Mirror، ومسار العمل الذي يلتقط ثم يُخزّن مؤقّتاً ثم يُعيد العرض خلفها.",null,false,"md",[1840,1841,1842,1843,1844,1845,1224,1846,1847,1848],"Vue 3","Nuxt","مُحمِّل Skeleton","حالات التحميل","getComputedStyle","Range API","TypeScript","مكتبة مكوّنات","إمكانية الوصول","ar",{},"\u002Fwrites-ar\u002Finside-a-skeleton-self-generating-skeleton-loaders",{"title":6,"description":1835},"writes-ar\u002Finside-a-skeleton-self-generating-skeleton-loaders","YF9r0lB5uhmEc4wHqor5kRgfbA9lR_dGeOudu2h0SQg",{"id":1856,"title":1857,"body":1858,"date":6417,"description":6418,"description_ar":1836,"draft":1837,"extension":1838,"image":1836,"keywords":6419,"lang":1849,"meta":6426,"navigation":324,"path":6427,"seo":6428,"stem":6429,"title_ar":1836,"updatedAt":6417,"__hash__":6430},"writesAr\u002Fwrites-ar\u002Fatell-input-the-library-rewrite.md","ملاحظات من إعادة كتابة ATellInput كمكتبة مُنشَرة",{"type":8,"value":1859,"toc":6407},[1860,1871,1874,1881,1884,1888,1891,1954,1969,1972,1989,1992,2079,2102,2252,2269,2280,2283,2287,2290,2378,2388,2403,2409,2972,2975,3003,3026,3044,3047,3050,3226,3240,3312,3315,3319,3326,3332,3666,3669,3740,3743,4072,4101,4108,4151,4162,4165,4249,4255,4259,4262,4273,4384,4406,4409,4413,4416,4419,4986,5028,5035,5175,5189,5192,5322,5325,5328,5465,5488,5491,5495,5524,5527,5760,5767,5993,5996,6170,6177,6180,6184,6190,6195,6317,6327,6330,6334,6345,6348,6351,6394,6397,6404],[11,1861,1862,1863,1866,1867,1870],{},"وُلد ",[18,1864,1865],{},"ATellInput"," داخل تطبيق واحد فقط: قيمة احتياطية تميل إلى السعودية أو مصر، ونداء داخلي لـ ",[18,1868,1869],{},"RestCountries","، وتحقّق يظهر عبر v-model لا غير، وقائمة منسدلة ظاهرة على الشاشة طوال الوقت. حلّت تلك النسخة مسألة الصحّة: اضطراب موضع المؤشّر، وأسباب التحقّق، ودعم RTL\u002FLTR. وأنجزت معظم ما يحتاجه مستهلك واحد.",[11,1872,1873],{},"هذه التدوينة تحكي ما جرى حين حاولتُ نشر المكوّن.",[11,1875,1876,1877,1880],{},"يعيش المكوّن الآن في ",[18,1878,1879],{},"@alikhalilll\u002Fui\u002Ftell-input","، ومهمّة المكتبة المُنشَرة تختلف كلياً. لا تستطيع أن تُشفِّر السوق الذي تخدمه داخل الكود. ولا أن تفترض شكل نظام تصميم المستهلك. ولا أن تُلزم كلّ نقطة تركيب بانتظار واجهة برمجية طرف ثالث. ولا حتى أن تعرف مسبقاً إن كان المستخدم على هاتف أم على حاسوب. حافظت إعادة الكتابة على صحّة النسخة الأصلية وأضافت ما يحتاجه مستهلك المكتبة فعلياً، وكلّ تغيير تقريباً وراءه علّة محدّدة أو قصور واضح في النسخة القديمة.",[11,1882,1883],{},"جمعتُ التغييرات حول السبعة التي استحقّت مكانها.",[26,1885,1887],{"id":1886},"نموذجان-بشكلين-مختلفين-الرقم-للخارج-وiso2-في-الداخل","نموذجان بشكلين مختلفين: الرقم للخارج، وISO2 في الداخل",[11,1889,1890],{},"بدت الواجهة القديمة أنيقة من بعيد:",[40,1892,1896],{"className":1893,"code":1894,"language":1895,"meta":45,"style":45},"language-typescript shiki shiki-themes github-light github-dark","const phoneModelValue = defineModel\u003Cstring>('phone', { required: true });\nconst countryModelValue = defineModel\u003Cstring>('country', { required: true });\n","typescript",[18,1897,1898,1928],{"__ignoreMap":45},[49,1899,1900,1902,1905,1907,1910,1912,1915,1917,1920,1923,1925],{"class":51,"line":52},[49,1901,330],{"class":63},[49,1903,1904],{"class":333}," phoneModelValue",[49,1906,343],{"class":63},[49,1908,1909],{"class":262}," defineModel",[49,1911,56],{"class":55},[49,1913,1914],{"class":333},"string",[49,1916,684],{"class":55},[49,1918,1919],{"class":70},"'phone'",[49,1921,1922],{"class":55},", { required: ",[49,1924,774],{"class":333},[49,1926,1927],{"class":55}," });\n",[49,1929,1930,1932,1935,1937,1939,1941,1943,1945,1948,1950,1952],{"class":51,"line":82},[49,1931,330],{"class":63},[49,1933,1934],{"class":333}," countryModelValue",[49,1936,343],{"class":63},[49,1938,1909],{"class":262},[49,1940,56],{"class":55},[49,1942,1914],{"class":333},[49,1944,684],{"class":55},[49,1946,1947],{"class":70},"'country'",[49,1949,1922],{"class":55},[49,1951,774],{"class":333},[49,1953,1927],{"class":55},[11,1955,1956,1957,1960,1961,1964,1965,1968],{},"كان ",[18,1958,1959],{},"country"," سلسلة نصية تحوي أرقام الاتصال: ",[18,1962,1963],{},"\"20\""," لمصر، و",[18,1966,1967],{},"\"44\""," للمملكة المتحدة. سهل التحويل إلى JSON، وسهل ترميزه في URL، وسهل إسكانه في عمود قاعدة بيانات.",[11,1970,1971],{},"المشكلة أنّه عجز عن التفريق بين الولايات المتحدة وكندا.",[11,1973,1974,1975,1978,1979,1982,1983,1985,1986,1988],{},"يتقاسم الرمز ",[18,1976,1977],{},"+1"," أكثر من 25 دولة ضمن NANP. لو اختار مستخدم كندي \"كندا\" من القائمة وأصدر v-model القيمة ",[18,1980,1981],{},"\"1\"",", فلن يعرف الأب أيّ ",[18,1984,1977],{}," هو المقصود. حدّث الصفحة، واستعد الحالة من URL، ستعود إليك ",[18,1987,1981],{}," بينما يعرض المنتقي أوّل دولة صادف أن ترتّبها القائمة. النموذج الذي يختصر الدولة في \"سلسلة رموز اتصال\" كان يُفقد نصف المعلومة عن شيء من قطعتين.",[11,1990,1991],{},"النموذج الجديد يفصل بين القطعتين:",[40,1993,1995],{"className":1893,"code":1994,"language":1895,"meta":45,"style":45},"const phone = defineModel\u003Cstring>('phone', { default: '' });\nconst country = defineModel\u003Cnumber | null>('country', { default: null });\n\nconst selectedIso2 = ref\u003Cstring>('');\n",[18,1996,1997,2024,2054,2058],{"__ignoreMap":45},[49,1998,1999,2001,2004,2006,2008,2010,2012,2014,2016,2019,2022],{"class":51,"line":52},[49,2000,330],{"class":63},[49,2002,2003],{"class":333}," phone",[49,2005,343],{"class":63},[49,2007,1909],{"class":262},[49,2009,56],{"class":55},[49,2011,1914],{"class":333},[49,2013,684],{"class":55},[49,2015,1919],{"class":70},[49,2017,2018],{"class":55},", { default: ",[49,2020,2021],{"class":70},"''",[49,2023,1927],{"class":55},[49,2025,2026,2028,2031,2033,2035,2037,2040,2042,2044,2046,2048,2050,2052],{"class":51,"line":82},[49,2027,330],{"class":63},[49,2029,2030],{"class":333}," country",[49,2032,343],{"class":63},[49,2034,1909],{"class":262},[49,2036,56],{"class":55},[49,2038,2039],{"class":333},"number",[49,2041,679],{"class":63},[49,2043,657],{"class":333},[49,2045,684],{"class":55},[49,2047,1947],{"class":70},[49,2049,2018],{"class":55},[49,2051,172],{"class":333},[49,2053,1927],{"class":55},[49,2055,2056],{"class":51,"line":88},[49,2057,325],{"emptyLinePlaceholder":324},[49,2059,2060,2062,2065,2067,2069,2071,2073,2075,2077],{"class":51,"line":94},[49,2061,330],{"class":63},[49,2063,2064],{"class":333}," selectedIso2",[49,2066,343],{"class":63},[49,2068,622],{"class":262},[49,2070,56],{"class":55},[49,2072,1914],{"class":333},[49,2074,684],{"class":55},[49,2076,2021],{"class":70},[49,2078,630],{"class":55},[11,2080,2081,2082,2084,2085,159,2088,159,2091,2093,2094,2097,2098,2101],{},"صار ",[18,2083,1959],{}," رقماً (",[18,2086,2087],{},"20",[18,2089,2090],{},"1",[18,2092,172],{},") لأنّ رموز الاتصال أرقامٌ في جوهرها، ولأنّ ",[18,2095,2096],{},"Number"," ينجو من رحلات URL وDB وJSON بلا أيّ التباس ناتج عن اقتطاع الأحرف. غير أنّ المكوّن يتتبّع داخلياً أيضاً ",[18,2099,2100],{},"selectedIso2"," يملكه المنتقي وحده. تبقى القيمتان متزامنتين عبر watcher-ين، أحدهما للخارج والآخر للداخل، والداخلي منهما فيه حارس صغير لكنّه حاسم:",[40,2103,2105],{"className":1893,"code":2104,"language":1895,"meta":45,"style":45},"watch(\n  country,\n  (next) => {\n    if (next == null) {\n      if (selectedIso2.value) selectedIso2.value = '';\n      return;\n    }\n    if (dialNumberFor(selectedIso2.value) === next) return;\n    const iso2 = resolveCountryIdentifier(String(next));\n    if (iso2) selectedIso2.value = iso2;\n  },\n  { immediate: true }\n);\n",[18,2106,2107,2115,2120,2134,2150,2165,2172,2177,2200,2221,2233,2238,2248],{"__ignoreMap":45},[49,2108,2109,2112],{"class":51,"line":52},[49,2110,2111],{"class":262},"watch",[49,2113,2114],{"class":55},"(\n",[49,2116,2117],{"class":51,"line":82},[49,2118,2119],{"class":55},"  country,\n",[49,2121,2122,2125,2128,2130,2132],{"class":51,"line":88},[49,2123,2124],{"class":55},"  (",[49,2126,2127],{"class":601},"next",[49,2129,807],{"class":55},[49,2131,648],{"class":63},[49,2133,1625],{"class":55},[49,2135,2136,2139,2142,2145,2147],{"class":51,"line":94},[49,2137,2138],{"class":63},"    if",[49,2140,2141],{"class":55}," (next ",[49,2143,2144],{"class":63},"==",[49,2146,657],{"class":333},[49,2148,2149],{"class":55},") {\n",[49,2151,2152,2155,2158,2160,2163],{"class":51,"line":363},[49,2153,2154],{"class":63},"      if",[49,2156,2157],{"class":55}," (selectedIso2.value) selectedIso2.value ",[49,2159,67],{"class":63},[49,2161,2162],{"class":70}," ''",[49,2164,319],{"class":55},[49,2166,2167,2170],{"class":51,"line":374},[49,2168,2169],{"class":63},"      return",[49,2171,319],{"class":55},[49,2173,2174],{"class":51,"line":385},[49,2175,2176],{"class":55},"    }\n",[49,2178,2179,2181,2184,2187,2190,2192,2195,2198],{"class":51,"line":662},[49,2180,2138],{"class":63},[49,2182,2183],{"class":55}," (",[49,2185,2186],{"class":262},"dialNumberFor",[49,2188,2189],{"class":55},"(selectedIso2.value) ",[49,2191,654],{"class":63},[49,2193,2194],{"class":55}," next) ",[49,2196,2197],{"class":63},"return",[49,2199,319],{"class":55},[49,2201,2202,2205,2208,2210,2213,2215,2218],{"class":51,"line":691},[49,2203,2204],{"class":63},"    const",[49,2206,2207],{"class":333}," iso2",[49,2209,343],{"class":63},[49,2211,2212],{"class":262}," resolveCountryIdentifier",[49,2214,625],{"class":55},[49,2216,2217],{"class":262},"String",[49,2219,2220],{"class":55},"(next));\n",[49,2222,2223,2225,2228,2230],{"class":51,"line":696},[49,2224,2138],{"class":63},[49,2226,2227],{"class":55}," (iso2) selectedIso2.value ",[49,2229,67],{"class":63},[49,2231,2232],{"class":55}," iso2;\n",[49,2234,2235],{"class":51,"line":723},[49,2236,2237],{"class":55},"  },\n",[49,2239,2240,2243,2245],{"class":51,"line":744},[49,2241,2242],{"class":55},"  { immediate: ",[49,2244,774],{"class":333},[49,2246,2247],{"class":55}," }\n",[49,2249,2250],{"class":51,"line":768},[49,2251,630],{"class":55},[11,2253,2254,2255,2258,2259,2262,2263,2265,2266,2268],{},"السطر ",[18,2256,2257],{},"if (dialNumberFor(selectedIso2.value) === next) return;"," هو ما أودّ التوقّف عنده. حين يكتب الأب ",[18,2260,2261],{},"country = 1"," من جديد إلى النموذج والمنتقي يعرض كندا مختارة أصلاً، ينطلق الـ watch, لكنّ رمز الاتصال متطابق مسبقاً، فيعود دون أن يمسّ ",[18,2264,2100],{},".\nلولا هذا الحارس، لكان الـ watcher حلّ ",[18,2267,1981],{}," إلى أوّل دولة NANP أبجدياً وقلب كندا بصمت إلى أيّاً كانت تلك الدولة. الحارس يحفظ الحالة الأغنى.",[11,2270,2271,2272,2275,2276,2279],{},"الـ watch الخارجي يعمل بوضع ",[18,2273,2274],{},"flush: 'sync'"," كي يبقى العلَم ",[18,2277,2278],{},"autoSettingCountry"," متّسقاً. لأنّه حين يختار المطابِق دولةً تلقائياً بناءً على ما كُتب، لا نريد اعتبار ذلك \"اختياراً يدوياً\" يُقفل الاكتشاف التلقائي لاحقاً.",[11,2281,2282],{},"نصفان من بيانات الدولة، ونموذجان مستقلّان، ومصدر حقيقة واحد لكلٍّ منهما.",[26,2284,2286],{"id":2285},"تجربة-تقوم-على-الاكتشاف-أولاً-المنتقي-مخفي-افتراضياً","تجربة تقوم على الاكتشاف أولاً: المنتقي مخفي افتراضياً",[11,2288,2289],{},"كان المكوّن القديم يرسم القائمة دائماً: العلَم يساراً، وحقل الإدخال يميناً. أمّا المكوّن الجديد فيكتفي بحقل الإدخال وحده افتراضياً. تنزلق القائمة إلى الظهور فقط حين يكتب المستخدم شيئاً يتعرّف عليه المطابِق:",[40,2291,2293],{"className":42,"code":2292,"language":44,"meta":45,"style":45},"\u003CTransition\n  enter-active-class=\"transition-all duration-200 ease-out overflow-hidden\"\n  leave-active-class=\"transition-all duration-150 ease-in overflow-hidden\"\n  enter-from-class=\"opacity-0 -translate-x-1 max-w-0\"\n  leave-to-class=\"opacity-0 -translate-x-1 max-w-0\"\n  enter-to-class=\"max-w-[12rem]\"\n  leave-from-class=\"max-w-[12rem]\"\n>\n  \u003CACountrySelect v-if=\"!props.detectFromInput || selectedIso2\" ... \u002F>\n\u003C\u002FTransition>\n",[18,2294,2295,2302,2312,2322,2332,2341,2351,2360,2364,2369],{"__ignoreMap":45},[49,2296,2297,2299],{"class":51,"line":52},[49,2298,56],{"class":55},[49,2300,2301],{"class":59},"Transition\n",[49,2303,2304,2307,2309],{"class":51,"line":82},[49,2305,2306],{"class":262},"  enter-active-class",[49,2308,67],{"class":55},[49,2310,2311],{"class":70},"\"transition-all duration-200 ease-out overflow-hidden\"\n",[49,2313,2314,2317,2319],{"class":51,"line":88},[49,2315,2316],{"class":262},"  leave-active-class",[49,2318,67],{"class":55},[49,2320,2321],{"class":70},"\"transition-all duration-150 ease-in overflow-hidden\"\n",[49,2323,2324,2327,2329],{"class":51,"line":94},[49,2325,2326],{"class":262},"  enter-from-class",[49,2328,67],{"class":55},[49,2330,2331],{"class":70},"\"opacity-0 -translate-x-1 max-w-0\"\n",[49,2333,2334,2337,2339],{"class":51,"line":363},[49,2335,2336],{"class":262},"  leave-to-class",[49,2338,67],{"class":55},[49,2340,2331],{"class":70},[49,2342,2343,2346,2348],{"class":51,"line":374},[49,2344,2345],{"class":262},"  enter-to-class",[49,2347,67],{"class":55},[49,2349,2350],{"class":70},"\"max-w-[12rem]\"\n",[49,2352,2353,2356,2358],{"class":51,"line":385},[49,2354,2355],{"class":262},"  leave-from-class",[49,2357,67],{"class":55},[49,2359,2350],{"class":70},[49,2361,2362],{"class":51,"line":662},[49,2363,79],{"class":55},[49,2365,2366],{"class":51,"line":691},[49,2367,2368],{"class":55},"  \u003CACountrySelect v-if=\"!props.detectFromInput || selectedIso2\" ... \u002F>\n",[49,2370,2371,2373,2376],{"class":51,"line":696},[49,2372,97],{"class":55},[49,2374,2375],{"class":59},"Transition",[49,2377,79],{"class":55},[11,2379,2380,2381,2384,2385,527],{},"يتقلّص ",[18,2382,2383],{},"max-w"," إلى صفر ما لم يكن هناك اختيار، ثم يتحرّك ليتفتّح عند أوّل تطابق ناجح. ومن يريد الشكل القديم يستطيع إعادة المنتقي الظاهر دائماً عبر ",[18,2386,2387],{},"detectFromInput=\"false\"",[11,2389,2390,2391,2394,2395,2398,2399,2402],{},"هذا سلوك ينحاز إلى رأي واضح. الأغلبية العظمى من المستخدمين في معظم الدول لا يكتبون بادئة ",[18,2392,2393],{},"+"," أبداً. بل يكتبون الرقم كما يكتبونه في هاتفهم: ",[18,2396,2397],{},"01066105963"," في القاهرة، و",[18,2400,2401],{},"07911 123456"," في مانشستر. القائمة التي \"يُجبَرون على التعامل معها\" في كلّ حقل هاتف آخر خطوة زائدة بالنسبة لهم. أخفِها إذاً. وراقب ما يكتبونه. ولا تُظهرها إلا حين يقع الاكتشاف على نتيجة.",[11,2404,2405,2406,337],{},"الجزء الوحيد الذي فيه بعض التعقيد هو \"مراقبة ما يكتبونه\". يمرّ عبر ثلاث طبقات في ",[18,2407,2408],{},"matchLeadingDialCode",[40,2410,2412],{"className":1893,"code":2411,"language":1895,"meta":45,"style":45},"function matchLeadingDialCode(digits: string): DialMatch | null {\n  if (!digits) return null;\n\n  \u002F\u002F Tier 1: international parse, disambiguates NANP and strips the calling code.\n  try {\n    const parsed = parsePhoneNumberFromString(`+${digits}`);\n    if (parsed?.country && parsed.countryCallingCode) {\n      const parsedCountry = getCountryByValue(parsed.country);\n      if (parsedCountry) {\n        return { country: parsedCountry, nationalNumber: String(parsed.nationalNumber ?? '') };\n      }\n    }\n  } catch {\n    \u002F* libphonenumber throws on partial input, fall through *\u002F\n  }\n\n  \u002F\u002F Tier 2: national-format parse using the silently-inferred country as a hint.\n  const hint = inferredCountry.value;\n  if (hint && digits.length >= 4) {\n    try {\n      const parsed = parsePhoneNumberFromString(digits, hint as CountryCode);\n      if (parsed?.isValid()) {\n        const matched = getCountryByValue(parsed.country || hint);\n        if (matched) {\n          return { country: matched, nationalNumber: String(parsed.nationalNumber ?? '') };\n        }\n      }\n    } catch {\n      \u002F* fall through *\u002F\n    }\n  }\n\n  \u002F\u002F Tier 3: longest-prefix match over our own dial-digits index.\n  for (let len = Math.min(3, digits.length); len >= 1; len--) {\n    const prefix = digits.slice(0, len);\n    const group = getCountriesByDial(prefix);\n    if (!group.length) continue;\n    const nationalNumber = digits.slice(prefix.length);\n    if (group.length === 1) return { country: group[0], nationalNumber };\n    \u002F\u002F ambiguity: prefer current selection, then recents, then first\n    \u002F\u002F (see \"Recents as the tiebreaker\" below)\n    return { country: group[0], nationalNumber };\n  }\n  return null;\n}\n",[18,2413,2414,2445,2464,2468,2474,2481,2504,2517,2533,2540,2561,2566,2570,2580,2585,2590,2594,2599,2612,2635,2642,2663,2676,2697,2705,2723,2729,2734,2744,2750,2755,2760,2765,2771,2820,2843,2859,2880,2901,2928,2934,2940,2952,2957,2967],{"__ignoreMap":45},[49,2415,2416,2419,2422,2424,2427,2429,2431,2434,2436,2439,2441,2443],{"class":51,"line":52},[49,2417,2418],{"class":63},"function",[49,2420,2421],{"class":262}," matchLeadingDialCode",[49,2423,625],{"class":55},[49,2425,2426],{"class":601},"digits",[49,2428,337],{"class":63},[49,2430,607],{"class":333},[49,2432,2433],{"class":55},")",[49,2435,337],{"class":63},[49,2437,2438],{"class":262}," DialMatch",[49,2440,679],{"class":63},[49,2442,657],{"class":333},[49,2444,1625],{"class":55},[49,2446,2447,2450,2452,2455,2458,2460,2462],{"class":51,"line":82},[49,2448,2449],{"class":63},"  if",[49,2451,2183],{"class":55},[49,2453,2454],{"class":63},"!",[49,2456,2457],{"class":55},"digits) ",[49,2459,2197],{"class":63},[49,2461,657],{"class":333},[49,2463,319],{"class":55},[49,2465,2466],{"class":51,"line":88},[49,2467,325],{"emptyLinePlaceholder":324},[49,2469,2470],{"class":51,"line":94},[49,2471,2473],{"class":2472},"sJ8bj","  \u002F\u002F Tier 1: international parse, disambiguates NANP and strips the calling code.\n",[49,2475,2476,2479],{"class":51,"line":363},[49,2477,2478],{"class":63},"  try",[49,2480,1625],{"class":55},[49,2482,2483,2485,2488,2490,2493,2495,2498,2500,2502],{"class":51,"line":374},[49,2484,2204],{"class":63},[49,2486,2487],{"class":333}," parsed",[49,2489,343],{"class":63},[49,2491,2492],{"class":262}," parsePhoneNumberFromString",[49,2494,625],{"class":55},[49,2496,2497],{"class":70},"`+${",[49,2499,2426],{"class":55},[49,2501,739],{"class":70},[49,2503,630],{"class":55},[49,2505,2506,2508,2511,2514],{"class":51,"line":385},[49,2507,2138],{"class":63},[49,2509,2510],{"class":55}," (parsed?.country ",[49,2512,2513],{"class":63},"&&",[49,2515,2516],{"class":55}," parsed.countryCallingCode) {\n",[49,2518,2519,2522,2525,2527,2530],{"class":51,"line":662},[49,2520,2521],{"class":63},"      const",[49,2523,2524],{"class":333}," parsedCountry",[49,2526,343],{"class":63},[49,2528,2529],{"class":262}," getCountryByValue",[49,2531,2532],{"class":55},"(parsed.country);\n",[49,2534,2535,2537],{"class":51,"line":691},[49,2536,2154],{"class":63},[49,2538,2539],{"class":55}," (parsedCountry) {\n",[49,2541,2542,2545,2548,2550,2553,2556,2558],{"class":51,"line":696},[49,2543,2544],{"class":63},"        return",[49,2546,2547],{"class":55}," { country: parsedCountry, nationalNumber: ",[49,2549,2217],{"class":262},[49,2551,2552],{"class":55},"(parsed.nationalNumber ",[49,2554,2555],{"class":63},"??",[49,2557,2162],{"class":70},[49,2559,2560],{"class":55},") };\n",[49,2562,2563],{"class":51,"line":723},[49,2564,2565],{"class":55},"      }\n",[49,2567,2568],{"class":51,"line":744},[49,2569,2176],{"class":55},[49,2571,2572,2575,2578],{"class":51,"line":768},[49,2573,2574],{"class":55},"  } ",[49,2576,2577],{"class":63},"catch",[49,2579,1625],{"class":55},[49,2581,2582],{"class":51,"line":779},[49,2583,2584],{"class":2472},"    \u002F* libphonenumber throws on partial input, fall through *\u002F\n",[49,2586,2587],{"class":51,"line":784},[49,2588,2589],{"class":55},"  }\n",[49,2591,2592],{"class":51,"line":789},[49,2593,325],{"emptyLinePlaceholder":324},[49,2595,2596],{"class":51,"line":820},[49,2597,2598],{"class":2472},"  \u002F\u002F Tier 2: national-format parse using the silently-inferred country as a hint.\n",[49,2600,2601,2604,2607,2609],{"class":51,"line":829},[49,2602,2603],{"class":63},"  const",[49,2605,2606],{"class":333}," hint",[49,2608,343],{"class":63},[49,2610,2611],{"class":55}," inferredCountry.value;\n",[49,2613,2614,2616,2619,2621,2624,2627,2630,2633],{"class":51,"line":834},[49,2615,2449],{"class":63},[49,2617,2618],{"class":55}," (hint ",[49,2620,2513],{"class":63},[49,2622,2623],{"class":55}," digits.",[49,2625,2626],{"class":333},"length",[49,2628,2629],{"class":63}," >=",[49,2631,2632],{"class":333}," 4",[49,2634,2149],{"class":55},[49,2636,2637,2640],{"class":51,"line":844},[49,2638,2639],{"class":63},"    try",[49,2641,1625],{"class":55},[49,2643,2644,2646,2648,2650,2652,2655,2658,2661],{"class":51,"line":861},[49,2645,2521],{"class":63},[49,2647,2487],{"class":333},[49,2649,343],{"class":63},[49,2651,2492],{"class":262},[49,2653,2654],{"class":55},"(digits, hint ",[49,2656,2657],{"class":63},"as",[49,2659,2660],{"class":262}," CountryCode",[49,2662,630],{"class":55},[49,2664,2665,2667,2670,2673],{"class":51,"line":888},[49,2666,2154],{"class":63},[49,2668,2669],{"class":55}," (parsed?.",[49,2671,2672],{"class":262},"isValid",[49,2674,2675],{"class":55},"()) {\n",[49,2677,2678,2681,2684,2686,2688,2691,2694],{"class":51,"line":906},[49,2679,2680],{"class":63},"        const",[49,2682,2683],{"class":333}," matched",[49,2685,343],{"class":63},[49,2687,2529],{"class":262},[49,2689,2690],{"class":55},"(parsed.country ",[49,2692,2693],{"class":63},"||",[49,2695,2696],{"class":55}," hint);\n",[49,2698,2699,2702],{"class":51,"line":927},[49,2700,2701],{"class":63},"        if",[49,2703,2704],{"class":55}," (matched) {\n",[49,2706,2707,2710,2713,2715,2717,2719,2721],{"class":51,"line":937},[49,2708,2709],{"class":63},"          return",[49,2711,2712],{"class":55}," { country: matched, nationalNumber: ",[49,2714,2217],{"class":262},[49,2716,2552],{"class":55},[49,2718,2555],{"class":63},[49,2720,2162],{"class":70},[49,2722,2560],{"class":55},[49,2724,2726],{"class":51,"line":2725},26,[49,2727,2728],{"class":55},"        }\n",[49,2730,2732],{"class":51,"line":2731},27,[49,2733,2565],{"class":55},[49,2735,2737,2740,2742],{"class":51,"line":2736},28,[49,2738,2739],{"class":55},"    } ",[49,2741,2577],{"class":63},[49,2743,1625],{"class":55},[49,2745,2747],{"class":51,"line":2746},29,[49,2748,2749],{"class":2472},"      \u002F* fall through *\u002F\n",[49,2751,2753],{"class":51,"line":2752},30,[49,2754,2176],{"class":55},[49,2756,2758],{"class":51,"line":2757},31,[49,2759,2589],{"class":55},[49,2761,2763],{"class":51,"line":2762},32,[49,2764,325],{"emptyLinePlaceholder":324},[49,2766,2768],{"class":51,"line":2767},33,[49,2769,2770],{"class":2472},"  \u002F\u002F Tier 3: longest-prefix match over our own dial-digits index.\n",[49,2772,2774,2777,2779,2782,2785,2787,2790,2793,2795,2798,2801,2803,2806,2809,2812,2815,2818],{"class":51,"line":2773},34,[49,2775,2776],{"class":63},"  for",[49,2778,2183],{"class":55},[49,2780,2781],{"class":63},"let",[49,2783,2784],{"class":55}," len ",[49,2786,67],{"class":63},[49,2788,2789],{"class":55}," Math.",[49,2791,2792],{"class":262},"min",[49,2794,625],{"class":55},[49,2796,2797],{"class":333},"3",[49,2799,2800],{"class":55},", digits.",[49,2802,2626],{"class":333},[49,2804,2805],{"class":55},"); len ",[49,2807,2808],{"class":63},">=",[49,2810,2811],{"class":333}," 1",[49,2813,2814],{"class":55},"; len",[49,2816,2817],{"class":63},"--",[49,2819,2149],{"class":55},[49,2821,2823,2825,2828,2830,2832,2835,2837,2840],{"class":51,"line":2822},35,[49,2824,2204],{"class":63},[49,2826,2827],{"class":333}," prefix",[49,2829,343],{"class":63},[49,2831,2623],{"class":55},[49,2833,2834],{"class":262},"slice",[49,2836,625],{"class":55},[49,2838,2839],{"class":333},"0",[49,2841,2842],{"class":55},", len);\n",[49,2844,2846,2848,2851,2853,2856],{"class":51,"line":2845},36,[49,2847,2204],{"class":63},[49,2849,2850],{"class":333}," group",[49,2852,343],{"class":63},[49,2854,2855],{"class":262}," getCountriesByDial",[49,2857,2858],{"class":55},"(prefix);\n",[49,2860,2862,2864,2866,2868,2871,2873,2875,2878],{"class":51,"line":2861},37,[49,2863,2138],{"class":63},[49,2865,2183],{"class":55},[49,2867,2454],{"class":63},[49,2869,2870],{"class":55},"group.",[49,2872,2626],{"class":333},[49,2874,807],{"class":55},[49,2876,2877],{"class":63},"continue",[49,2879,319],{"class":55},[49,2881,2883,2885,2888,2890,2892,2894,2897,2899],{"class":51,"line":2882},38,[49,2884,2204],{"class":63},[49,2886,2887],{"class":333}," nationalNumber",[49,2889,343],{"class":63},[49,2891,2623],{"class":55},[49,2893,2834],{"class":262},[49,2895,2896],{"class":55},"(prefix.",[49,2898,2626],{"class":333},[49,2900,630],{"class":55},[49,2902,2904,2906,2909,2911,2914,2916,2918,2920,2923,2925],{"class":51,"line":2903},39,[49,2905,2138],{"class":63},[49,2907,2908],{"class":55}," (group.",[49,2910,2626],{"class":333},[49,2912,2913],{"class":63}," ===",[49,2915,2811],{"class":333},[49,2917,807],{"class":55},[49,2919,2197],{"class":63},[49,2921,2922],{"class":55}," { country: group[",[49,2924,2839],{"class":333},[49,2926,2927],{"class":55},"], nationalNumber };\n",[49,2929,2931],{"class":51,"line":2930},40,[49,2932,2933],{"class":2472},"    \u002F\u002F ambiguity: prefer current selection, then recents, then first\n",[49,2935,2937],{"class":51,"line":2936},41,[49,2938,2939],{"class":2472},"    \u002F\u002F (see \"Recents as the tiebreaker\" below)\n",[49,2941,2943,2946,2948,2950],{"class":51,"line":2942},42,[49,2944,2945],{"class":63},"    return",[49,2947,2922],{"class":55},[49,2949,2839],{"class":333},[49,2951,2927],{"class":55},[49,2953,2955],{"class":51,"line":2954},43,[49,2956,2589],{"class":55},[49,2958,2960,2963,2965],{"class":51,"line":2959},44,[49,2961,2962],{"class":63},"  return",[49,2964,657],{"class":333},[49,2966,319],{"class":55},[49,2968,2970],{"class":51,"line":2969},45,[49,2971,1719],{"class":55},[11,2973,2974],{},"توجد الطبقات لأنّ لكلّ واحدة منها فئة من المدخلات تعجز الطبقتان الأخريان عن معالجتها.",[11,2976,2977,2978,2980,2981,2984,2985,2988,2989,2992,2993,2995,2996,2998,2999,3002],{},"تأخذ الطبقة الأولى ما كتبه المستخدم، وتُلحق قبله ",[18,2979,2393],{},", ثم تطلب من ",[18,2982,2983],{},"libphonenumber"," تحليله بوصفه رقماً دولياً. إن كتب المستخدم ",[18,2986,2987],{},"447911123456"," سيتطابق مع المملكة المتحدة. وإن كتب ",[18,2990,2991],{},"1416..."," سيتطابق مع كندا تحديداً (لا مع NANP العام)، لأنّ ",[18,2994,2983],{}," يحمل قواعد رموز المناطق مدمجة أصلاً. توجد كتلة catch لأنّ ",[18,2997,2983],{}," يرمي استثناءً على الإدخال الجزئي. ",[18,3000,3001],{},"447"," رقم ناقص؛ يرفع التحليل استثناءً، فننتقل إلى الطبقة التالية.",[11,3004,3005,3006,3008,3009,3012,3013,1446,3015,3017,3018,3021,3022,3025],{},"الطبقة الثانية هي المسار المخصّص لمن يكتب الرقم كما يستعمله في بلده. ",[18,3007,2397],{}," لا يبدأ برمز اتصال، وبالتالي لا نفع من الطبقة الأولى. لكن إن كنّا نعرف بصمت أنّ المستخدم في مصر على الأرجح (اعتماداً على IP أو timezone, والمزيد عن هذا لاحقاً)، فبإمكاننا تمرير ",[18,3010,3011],{},"\"EG\""," تلميحاً للمحلِّل، وسيتعرّف ",[18,3014,2983],{},[18,3016,2397],{}," بوصفه رقم هاتف محمول مصري صحيح، ويجرّد الصفر الأول (البادئة الوطنية لمصر)، ويعيد الرقم الوطني المعياري ",[18,3019,3020],{},"1066105963",". الحارس ",[18,3023,3024],{},"digits.length >= 4"," يمنع المحلِّل من التعامل مع بدايات رموز المناطق ذات الرقمين بوصفها أرقاماً كاملة.",[11,3027,3028,3029,3032,3033,3035,3036,3039,3040,3043],{},"الطبقة الثالثة هي الاحتياطي لحالة \"كتبتُ ",[18,3030,3031],{},"44"," فقط ولا شيء غير ذلك\". في هذه الحالة لا مسار في ",[18,3034,2983],{}," يستطيع المطابقة، فالمدخل قصير جداً. لكنّ فهرس أرقام الاتصال الخاصّ بنا يكفي: ",[18,3037,3038],{},"byDialDigits.get('44')"," يعيد مجموعة المملكة المتحدة بطول 1, وينتهي الأمر. هنا تُثبت بنية ",[18,3041,3042],{},"Map\u003Cstring, CountryOption[]>"," الموروثة من النسخة القديمة قيمتها.",[11,3045,3046],{},"قد تبدو ثلاث طبقات كثيرة. في الحقيقة هي الحدّ الأدنى: كلّ طبقة تعالج مدخلات ترفضها الاثنتان الأخريان.",[11,3048,3049],{},"المُعالِج الذي يستدعيها يبقى صغيراً:",[40,3051,3053],{"className":1893,"code":3052,"language":1895,"meta":45,"style":45},"const detectAndApply = useDebounceFn(\n  () => {\n    if (!props.detectFromInput) return;\n    if (userPickedCountry.value || selectedIso2.value) return;\n    const current = phone.value;\n    if (!current) return;\n    const match = matchLeadingDialCode(current);\n    if (!match) return;\n    autoSettingCountry.value = true;\n    selectedIso2.value = match.country.value;\n    phone.value = match.nationalNumber;\n  },\n  computed(() => Math.max(0, props.detectDebounceMs))\n);\n",[18,3054,3055,3069,3078,3093,3109,3121,3136,3150,3165,3177,3187,3197,3201,3222],{"__ignoreMap":45},[49,3056,3057,3059,3062,3064,3067],{"class":51,"line":52},[49,3058,330],{"class":63},[49,3060,3061],{"class":333}," detectAndApply",[49,3063,343],{"class":63},[49,3065,3066],{"class":262}," useDebounceFn",[49,3068,2114],{"class":55},[49,3070,3071,3074,3076],{"class":51,"line":82},[49,3072,3073],{"class":55},"  () ",[49,3075,648],{"class":63},[49,3077,1625],{"class":55},[49,3079,3080,3082,3084,3086,3089,3091],{"class":51,"line":88},[49,3081,2138],{"class":63},[49,3083,2183],{"class":55},[49,3085,2454],{"class":63},[49,3087,3088],{"class":55},"props.detectFromInput) ",[49,3090,2197],{"class":63},[49,3092,319],{"class":55},[49,3094,3095,3097,3100,3102,3105,3107],{"class":51,"line":94},[49,3096,2138],{"class":63},[49,3098,3099],{"class":55}," (userPickedCountry.value ",[49,3101,2693],{"class":63},[49,3103,3104],{"class":55}," selectedIso2.value) ",[49,3106,2197],{"class":63},[49,3108,319],{"class":55},[49,3110,3111,3113,3116,3118],{"class":51,"line":363},[49,3112,2204],{"class":63},[49,3114,3115],{"class":333}," current",[49,3117,343],{"class":63},[49,3119,3120],{"class":55}," phone.value;\n",[49,3122,3123,3125,3127,3129,3132,3134],{"class":51,"line":374},[49,3124,2138],{"class":63},[49,3126,2183],{"class":55},[49,3128,2454],{"class":63},[49,3130,3131],{"class":55},"current) ",[49,3133,2197],{"class":63},[49,3135,319],{"class":55},[49,3137,3138,3140,3143,3145,3147],{"class":51,"line":385},[49,3139,2204],{"class":63},[49,3141,3142],{"class":333}," match",[49,3144,343],{"class":63},[49,3146,2421],{"class":262},[49,3148,3149],{"class":55},"(current);\n",[49,3151,3152,3154,3156,3158,3161,3163],{"class":51,"line":662},[49,3153,2138],{"class":63},[49,3155,2183],{"class":55},[49,3157,2454],{"class":63},[49,3159,3160],{"class":55},"match) ",[49,3162,2197],{"class":63},[49,3164,319],{"class":55},[49,3166,3167,3170,3172,3175],{"class":51,"line":691},[49,3168,3169],{"class":55},"    autoSettingCountry.value ",[49,3171,67],{"class":63},[49,3173,3174],{"class":333}," true",[49,3176,319],{"class":55},[49,3178,3179,3182,3184],{"class":51,"line":696},[49,3180,3181],{"class":55},"    selectedIso2.value ",[49,3183,67],{"class":63},[49,3185,3186],{"class":55}," match.country.value;\n",[49,3188,3189,3192,3194],{"class":51,"line":723},[49,3190,3191],{"class":55},"    phone.value ",[49,3193,67],{"class":63},[49,3195,3196],{"class":55}," match.nationalNumber;\n",[49,3198,3199],{"class":51,"line":744},[49,3200,2237],{"class":55},[49,3202,3203,3206,3208,3210,3212,3215,3217,3219],{"class":51,"line":768},[49,3204,3205],{"class":262},"  computed",[49,3207,645],{"class":55},[49,3209,648],{"class":63},[49,3211,2789],{"class":55},[49,3213,3214],{"class":262},"max",[49,3216,625],{"class":55},[49,3218,2839],{"class":333},[49,3220,3221],{"class":55},", props.detectDebounceMs))\n",[49,3223,3224],{"class":51,"line":779},[49,3225,630],{"class":55},[11,3227,3228,3229,1232,3232,3235,3236,3239],{},"يقرأ ",[18,3230,3231],{},"useDebounceFn",[18,3233,3234],{},"phone.value"," لحظة إطلاق المؤقّت، لا لحظة جدولته، فتذوب دفعة من الضغطات على المفاتيح في تحليل واحد. ويمنع ",[18,3237,3238],{},"userPickedCountry.value"," المكتشفَ التلقائيّ من الدهس على اختيار يدوي. متى استعمل المستخدم القائمة، ينسحب المطابِق مباشرة. أمّا مسح الحقل فلا يخضع لأيّ debounce, بل يعيد المنتقي إلى الاختفاء فوراً:",[40,3241,3243],{"className":1893,"code":3242,"language":1895,"meta":45,"style":45},"if (!cleaned) {\n  autoSettingCountry.value = true;\n  selectedIso2.value = '';\n  phone.value = '';\n  userPickedCountry.value = false;\n  return;\n}\n",[18,3244,3245,3257,3268,3279,3290,3302,3308],{"__ignoreMap":45},[49,3246,3247,3250,3252,3254],{"class":51,"line":52},[49,3248,3249],{"class":63},"if",[49,3251,2183],{"class":55},[49,3253,2454],{"class":63},[49,3255,3256],{"class":55},"cleaned) {\n",[49,3258,3259,3262,3264,3266],{"class":51,"line":82},[49,3260,3261],{"class":55},"  autoSettingCountry.value ",[49,3263,67],{"class":63},[49,3265,3174],{"class":333},[49,3267,319],{"class":55},[49,3269,3270,3273,3275,3277],{"class":51,"line":88},[49,3271,3272],{"class":55},"  selectedIso2.value ",[49,3274,67],{"class":63},[49,3276,2162],{"class":70},[49,3278,319],{"class":55},[49,3280,3281,3284,3286,3288],{"class":51,"line":94},[49,3282,3283],{"class":55},"  phone.value ",[49,3285,67],{"class":63},[49,3287,2162],{"class":70},[49,3289,319],{"class":55},[49,3291,3292,3295,3297,3300],{"class":51,"line":363},[49,3293,3294],{"class":55},"  userPickedCountry.value ",[49,3296,67],{"class":63},[49,3298,3299],{"class":333}," false",[49,3301,319],{"class":55},[49,3303,3304,3306],{"class":51,"line":374},[49,3305,2962],{"class":63},[49,3307,319],{"class":55},[49,3309,3310],{"class":51,"line":385},[49,3311,1719],{"class":55},[11,3313,3314],{},"تأخير المسح يعني بقاء القائمة معلّقة 150 ميلي ثانية بعد إفراغ الحقل، وهو ما يبدو خاطئاً في كلّ مرّة.",[26,3316,3318],{"id":3317},"سلسلة-اكتشاف-البيئة-ولماذا-هي-قابلة-للاستبدال","سلسلة اكتشاف البيئة، ولماذا هي قابلة للاستبدال",[11,3320,3321,3322,3325],{},"تعتمد الطبقة الثانية أعلاه على ",[18,3323,3324],{},"inferredCountry.value","، أي أفضل تخمين للمكوّن عن موقع المستخدم، يُضبط بصمت قبل أن يكتب حرفاً واحداً. كان \"أفضل تخمين\" في النسخة القديمة قيمة احتياطية مثبّتة على السعودية أو مصر، لأنّهما السوقان اللذان استهدفتهما النسخة الداخلية. المكتبة لا تملك ترف هذا الافتراض.",[11,3327,3328,3329,337],{},"السلسلة الجديدة هي ",[18,3330,3331],{},"detectCountry()",[40,3333,3335],{"className":1893,"code":3334,"language":1895,"meta":45,"style":45},"export async function detectCountry(opts: DetectCountryOptions = {}): Promise\u003Cstring> {\n  const {\n    strategy = 'auto',\n    ipEndpoint = 'https:\u002F\u002Fipapi.co\u002Fjson\u002F',\n    defaultCountry = 'US',\n    timeoutMs = 2000,\n    cache = true,\n  } = opts;\n\n  if (cache) {\n    const cached = readCache();\n    if (cached) return cached;\n  }\n\n  if (strategy === 'none') {\n    return defaultCountry.toUpperCase();\n  }\n\n  if (strategy === 'auto') {\n    const ipResult = await tryIp(ipEndpoint, timeoutMs);\n    if (ipResult) {\n      if (cache) writeCache(ipResult);\n      return ipResult;\n    }\n  }\n\n  const localResult = tryTimezone() ?? tryLocale();\n  const final = (localResult ?? defaultCountry).toUpperCase();\n  if (cache) writeCache(final);\n  return final;\n}\n",[18,3336,3337,3378,3384,3396,3408,3420,3432,3443,3452,3456,3463,3478,3490,3494,3498,3512,3524,3528,3532,3544,3562,3569,3582,3589,3593,3597,3601,3623,3644,3655,3662],{"__ignoreMap":45},[49,3338,3339,3342,3345,3348,3351,3353,3356,3358,3361,3363,3366,3368,3371,3373,3375],{"class":51,"line":52},[49,3340,3341],{"class":63},"export",[49,3343,3344],{"class":63}," async",[49,3346,3347],{"class":63}," function",[49,3349,3350],{"class":262}," detectCountry",[49,3352,625],{"class":55},[49,3354,3355],{"class":601},"opts",[49,3357,337],{"class":63},[49,3359,3360],{"class":262}," DetectCountryOptions",[49,3362,343],{"class":63},[49,3364,3365],{"class":55}," {})",[49,3367,337],{"class":63},[49,3369,3370],{"class":262}," Promise",[49,3372,56],{"class":55},[49,3374,1914],{"class":333},[49,3376,3377],{"class":55},"> {\n",[49,3379,3380,3382],{"class":51,"line":82},[49,3381,2603],{"class":63},[49,3383,1625],{"class":55},[49,3385,3386,3389,3391,3394],{"class":51,"line":88},[49,3387,3388],{"class":333},"    strategy",[49,3390,343],{"class":63},[49,3392,3393],{"class":70}," 'auto'",[49,3395,360],{"class":55},[49,3397,3398,3401,3403,3406],{"class":51,"line":94},[49,3399,3400],{"class":333},"    ipEndpoint",[49,3402,343],{"class":63},[49,3404,3405],{"class":70}," 'https:\u002F\u002Fipapi.co\u002Fjson\u002F'",[49,3407,360],{"class":55},[49,3409,3410,3413,3415,3418],{"class":51,"line":363},[49,3411,3412],{"class":333},"    defaultCountry",[49,3414,343],{"class":63},[49,3416,3417],{"class":70}," 'US'",[49,3419,360],{"class":55},[49,3421,3422,3425,3427,3430],{"class":51,"line":374},[49,3423,3424],{"class":333},"    timeoutMs",[49,3426,343],{"class":63},[49,3428,3429],{"class":333}," 2000",[49,3431,360],{"class":55},[49,3433,3434,3437,3439,3441],{"class":51,"line":385},[49,3435,3436],{"class":333},"    cache",[49,3438,343],{"class":63},[49,3440,3174],{"class":333},[49,3442,360],{"class":55},[49,3444,3445,3447,3449],{"class":51,"line":662},[49,3446,2574],{"class":55},[49,3448,67],{"class":63},[49,3450,3451],{"class":55}," opts;\n",[49,3453,3454],{"class":51,"line":691},[49,3455,325],{"emptyLinePlaceholder":324},[49,3457,3458,3460],{"class":51,"line":696},[49,3459,2449],{"class":63},[49,3461,3462],{"class":55}," (cache) {\n",[49,3464,3465,3467,3470,3472,3475],{"class":51,"line":723},[49,3466,2204],{"class":63},[49,3468,3469],{"class":333}," cached",[49,3471,343],{"class":63},[49,3473,3474],{"class":262}," readCache",[49,3476,3477],{"class":55},"();\n",[49,3479,3480,3482,3485,3487],{"class":51,"line":744},[49,3481,2138],{"class":63},[49,3483,3484],{"class":55}," (cached) ",[49,3486,2197],{"class":63},[49,3488,3489],{"class":55}," cached;\n",[49,3491,3492],{"class":51,"line":768},[49,3493,2589],{"class":55},[49,3495,3496],{"class":51,"line":779},[49,3497,325],{"emptyLinePlaceholder":324},[49,3499,3500,3502,3505,3507,3510],{"class":51,"line":784},[49,3501,2449],{"class":63},[49,3503,3504],{"class":55}," (strategy ",[49,3506,654],{"class":63},[49,3508,3509],{"class":70}," 'none'",[49,3511,2149],{"class":55},[49,3513,3514,3516,3519,3522],{"class":51,"line":789},[49,3515,2945],{"class":63},[49,3517,3518],{"class":55}," defaultCountry.",[49,3520,3521],{"class":262},"toUpperCase",[49,3523,3477],{"class":55},[49,3525,3526],{"class":51,"line":820},[49,3527,2589],{"class":55},[49,3529,3530],{"class":51,"line":829},[49,3531,325],{"emptyLinePlaceholder":324},[49,3533,3534,3536,3538,3540,3542],{"class":51,"line":834},[49,3535,2449],{"class":63},[49,3537,3504],{"class":55},[49,3539,654],{"class":63},[49,3541,3393],{"class":70},[49,3543,2149],{"class":55},[49,3545,3546,3548,3551,3553,3556,3559],{"class":51,"line":844},[49,3547,2204],{"class":63},[49,3549,3550],{"class":333}," ipResult",[49,3552,343],{"class":63},[49,3554,3555],{"class":63}," await",[49,3557,3558],{"class":262}," tryIp",[49,3560,3561],{"class":55},"(ipEndpoint, timeoutMs);\n",[49,3563,3564,3566],{"class":51,"line":861},[49,3565,2138],{"class":63},[49,3567,3568],{"class":55}," (ipResult) {\n",[49,3570,3571,3573,3576,3579],{"class":51,"line":888},[49,3572,2154],{"class":63},[49,3574,3575],{"class":55}," (cache) ",[49,3577,3578],{"class":262},"writeCache",[49,3580,3581],{"class":55},"(ipResult);\n",[49,3583,3584,3586],{"class":51,"line":906},[49,3585,2169],{"class":63},[49,3587,3588],{"class":55}," ipResult;\n",[49,3590,3591],{"class":51,"line":927},[49,3592,2176],{"class":55},[49,3594,3595],{"class":51,"line":937},[49,3596,2589],{"class":55},[49,3598,3599],{"class":51,"line":2725},[49,3600,325],{"emptyLinePlaceholder":324},[49,3602,3603,3605,3608,3610,3613,3616,3618,3621],{"class":51,"line":2731},[49,3604,2603],{"class":63},[49,3606,3607],{"class":333}," localResult",[49,3609,343],{"class":63},[49,3611,3612],{"class":262}," tryTimezone",[49,3614,3615],{"class":55},"() ",[49,3617,2555],{"class":63},[49,3619,3620],{"class":262}," tryLocale",[49,3622,3477],{"class":55},[49,3624,3625,3627,3630,3632,3635,3637,3640,3642],{"class":51,"line":2736},[49,3626,2603],{"class":63},[49,3628,3629],{"class":333}," final",[49,3631,343],{"class":63},[49,3633,3634],{"class":55}," (localResult ",[49,3636,2555],{"class":63},[49,3638,3639],{"class":55}," defaultCountry).",[49,3641,3521],{"class":262},[49,3643,3477],{"class":55},[49,3645,3646,3648,3650,3652],{"class":51,"line":2746},[49,3647,2449],{"class":63},[49,3649,3575],{"class":55},[49,3651,3578],{"class":262},[49,3653,3654],{"class":55},"(final);\n",[49,3656,3657,3659],{"class":51,"line":2752},[49,3658,2962],{"class":63},[49,3660,3661],{"class":55}," final;\n",[49,3663,3664],{"class":51,"line":2757},[49,3665,1719],{"class":55},[11,3667,3668],{},"أربع إشارات تُقيَّم بترتيب الكلفة: الأرخص المخبَّأ أولاً، والأغلى الذي يمرّ عبر الشبكة أخيراً.",[3670,3671,3672,3680,3691,3719,3733],"ol",{},[3673,3674,3675,3676,3679],"li",{},"تخزين مؤقّت في ",[18,3677,3678],{},"sessionStorage",". المستخدم حسم دولته أصلاً ضمن هذه الجلسة، فلا داعي لتكرار العمل.",[3673,3681,3682,3683,3686,3687,3690],{},"تحديد الموقع الجغرافي عبر IP باستخدام ",[18,3684,3685],{},"ipapi.co"," (قابل للتكوين). أدقّ إشارة، لكنّها تكلّف نداء شبكة وقدراً يسيراً من الخصوصية. من لا يريد هذا الطلب يمرّر ",[18,3688,3689],{},"strategy: 'locale'"," ليتخطّى الخطوة كلياً.",[3673,3692,3693,3694,3697,3698,3701,3702,3705,3706,3701,3709,3705,3712,3701,3715,3718],{},"المنطقة الزمنية عبر ",[18,3695,3696],{},"Intl.DateTimeFormat().resolvedOptions().timeZone",", يُبحث عنها في جدول صنعتُه يدوياً يربط ",[18,3699,3700],{},"Africa\u002FCairo"," بـ ",[18,3703,3704],{},"EG",", و",[18,3707,3708],{},"Asia\u002FRiyadh",[18,3710,3711],{},"SA",[18,3713,3714],{},"Europe\u002FLondon",[18,3716,3717],{},"GB",", وهكذا. يغطّي الجدول أكثف المناطق سكاناً، وهو صريح بأنّه لا يغطّي كلّ منطقة. عدم التطابق يمرّ إلى ما بعده.",[3673,3720,3721,3724,3725,3728,3729,3732],{},[18,3722,3723],{},"navigator.language"," الذي يعطي ",[18,3726,3727],{},"en-EG"," أو ",[18,3730,3731],{},"ar-SA"," وأشباهها. لاحقة المنطقة هي الدولة. أقلّ موثوقية (فالمستخدمون يتنقّلون بينما locale المتصفح لا)، لكنّها بلا كلفة.",[3673,3734,3735,3736,3739],{},"القيمة الافتراضية التي يمرّرها المستدعي. ",[18,3737,3738],{},"defaultCountry: 'US'"," هو الافتراضي في المكتبة، وللمستهلك حرية اختيار غيره.",[11,3741,3742],{},"خطوة IP هي التي احتاجت عناية حقيقية، لأنّ الطلب قد يتعلّق:",[40,3744,3746],{"className":1893,"code":3745,"language":1895,"meta":45,"style":45},"async function tryIp(endpoint: string, timeoutMs: number): Promise\u003Cstring | null> {\n  if (!isBrowser() || typeof fetch !== 'function') return null;\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), timeoutMs);\n  try {\n    const res = await fetch(endpoint, { signal: controller.signal, credentials: 'omit' });\n    if (!res.ok) return null;\n    const data = (await res.json()) as { country_code?: string; country?: string };\n    const code = (data.country_code ?? data.country ?? '').toString().toUpperCase();\n    return \u002F^[A-Z]{2}$\u002F.test(code) ? code : null;\n  } catch {\n    return null;\n  } finally {\n    clearTimeout(timer);\n  }\n}\n",[18,3747,3748,3792,3827,3844,3869,3875,3897,3914,3961,3994,4031,4039,4047,4056,4064,4068],{"__ignoreMap":45},[49,3749,3750,3753,3755,3757,3759,3762,3764,3766,3768,3771,3773,3776,3778,3780,3782,3784,3786,3788,3790],{"class":51,"line":52},[49,3751,3752],{"class":63},"async",[49,3754,3347],{"class":63},[49,3756,3558],{"class":262},[49,3758,625],{"class":55},[49,3760,3761],{"class":601},"endpoint",[49,3763,337],{"class":63},[49,3765,607],{"class":333},[49,3767,159],{"class":55},[49,3769,3770],{"class":601},"timeoutMs",[49,3772,337],{"class":63},[49,3774,3775],{"class":333}," number",[49,3777,2433],{"class":55},[49,3779,337],{"class":63},[49,3781,3370],{"class":262},[49,3783,56],{"class":55},[49,3785,1914],{"class":333},[49,3787,679],{"class":63},[49,3789,657],{"class":333},[49,3791,3377],{"class":55},[49,3793,3794,3796,3798,3800,3803,3805,3807,3810,3813,3816,3819,3821,3823,3825],{"class":51,"line":82},[49,3795,2449],{"class":63},[49,3797,2183],{"class":55},[49,3799,2454],{"class":63},[49,3801,3802],{"class":262},"isBrowser",[49,3804,3615],{"class":55},[49,3806,2693],{"class":63},[49,3808,3809],{"class":63}," typeof",[49,3811,3812],{"class":55}," fetch ",[49,3814,3815],{"class":63},"!==",[49,3817,3818],{"class":70}," 'function'",[49,3820,807],{"class":55},[49,3822,2197],{"class":63},[49,3824,657],{"class":333},[49,3826,319],{"class":55},[49,3828,3829,3831,3834,3836,3839,3842],{"class":51,"line":88},[49,3830,2603],{"class":63},[49,3832,3833],{"class":333}," controller",[49,3835,343],{"class":63},[49,3837,3838],{"class":63}," new",[49,3840,3841],{"class":262}," AbortController",[49,3843,3477],{"class":55},[49,3845,3846,3848,3851,3853,3856,3858,3860,3863,3866],{"class":51,"line":94},[49,3847,2603],{"class":63},[49,3849,3850],{"class":333}," timer",[49,3852,343],{"class":63},[49,3854,3855],{"class":262}," setTimeout",[49,3857,645],{"class":55},[49,3859,648],{"class":63},[49,3861,3862],{"class":55}," controller.",[49,3864,3865],{"class":262},"abort",[49,3867,3868],{"class":55},"(), timeoutMs);\n",[49,3870,3871,3873],{"class":51,"line":363},[49,3872,2478],{"class":63},[49,3874,1625],{"class":55},[49,3876,3877,3879,3882,3884,3886,3889,3892,3895],{"class":51,"line":374},[49,3878,2204],{"class":63},[49,3880,3881],{"class":333}," res",[49,3883,343],{"class":63},[49,3885,3555],{"class":63},[49,3887,3888],{"class":262}," fetch",[49,3890,3891],{"class":55},"(endpoint, { signal: controller.signal, credentials: ",[49,3893,3894],{"class":70},"'omit'",[49,3896,1927],{"class":55},[49,3898,3899,3901,3903,3905,3908,3910,3912],{"class":51,"line":385},[49,3900,2138],{"class":63},[49,3902,2183],{"class":55},[49,3904,2454],{"class":63},[49,3906,3907],{"class":55},"res.ok) ",[49,3909,2197],{"class":63},[49,3911,657],{"class":333},[49,3913,319],{"class":55},[49,3915,3916,3918,3921,3923,3925,3928,3931,3934,3937,3939,3941,3944,3947,3949,3952,3954,3956,3958],{"class":51,"line":662},[49,3917,2204],{"class":63},[49,3919,3920],{"class":333}," data",[49,3922,343],{"class":63},[49,3924,2183],{"class":55},[49,3926,3927],{"class":63},"await",[49,3929,3930],{"class":55}," res.",[49,3932,3933],{"class":262},"json",[49,3935,3936],{"class":55},"()) ",[49,3938,2657],{"class":63},[49,3940,701],{"class":55},[49,3942,3943],{"class":601},"country_code",[49,3945,3946],{"class":63},"?:",[49,3948,607],{"class":333},[49,3950,3951],{"class":55},"; ",[49,3953,1959],{"class":601},[49,3955,3946],{"class":63},[49,3957,607],{"class":333},[49,3959,3960],{"class":55}," };\n",[49,3962,3963,3965,3968,3970,3973,3975,3978,3980,3982,3984,3987,3990,3992],{"class":51,"line":691},[49,3964,2204],{"class":63},[49,3966,3967],{"class":333}," code",[49,3969,343],{"class":63},[49,3971,3972],{"class":55}," (data.country_code ",[49,3974,2555],{"class":63},[49,3976,3977],{"class":55}," data.country ",[49,3979,2555],{"class":63},[49,3981,2162],{"class":70},[49,3983,1251],{"class":55},[49,3985,3986],{"class":262},"toString",[49,3988,3989],{"class":55},"().",[49,3991,3521],{"class":262},[49,3993,3477],{"class":55},[49,3995,3996,3998,4000,4003,4006,4009,4012,4014,4017,4020,4022,4025,4027,4029],{"class":51,"line":696},[49,3997,2945],{"class":63},[49,3999,1117],{"class":70},[49,4001,4002],{"class":63},"^",[49,4004,4005],{"class":333},"[A-Z]",[49,4007,4008],{"class":63},"{2}$",[49,4010,4011],{"class":70},"\u002F",[49,4013,527],{"class":55},[49,4015,4016],{"class":262},"test",[49,4018,4019],{"class":55},"(code) ",[49,4021,758],{"class":63},[49,4023,4024],{"class":55}," code ",[49,4026,337],{"class":63},[49,4028,657],{"class":333},[49,4030,319],{"class":55},[49,4032,4033,4035,4037],{"class":51,"line":723},[49,4034,2574],{"class":55},[49,4036,2577],{"class":63},[49,4038,1625],{"class":55},[49,4040,4041,4043,4045],{"class":51,"line":744},[49,4042,2945],{"class":63},[49,4044,657],{"class":333},[49,4046,319],{"class":55},[49,4048,4049,4051,4054],{"class":51,"line":768},[49,4050,2574],{"class":55},[49,4052,4053],{"class":63},"finally",[49,4055,1625],{"class":55},[49,4057,4058,4061],{"class":51,"line":779},[49,4059,4060],{"class":262},"    clearTimeout",[49,4062,4063],{"class":55},"(timer);\n",[49,4065,4066],{"class":51,"line":784},[49,4067,2589],{"class":55},[49,4069,4070],{"class":51,"line":789},[49,4071,1719],{"class":55},[11,4073,4074,4075,4078,4079,4081,4082,4085,4086,4089,4090,4093,4094,4097,4098,4100],{},"يعني ",[18,4076,4077],{},"AbortController"," مع مؤقّت من ثانيتين أنّ خدمة IP بطيئة لا تستطيع تعليق سلسلة الاكتشاف لأكثر من ",[18,4080,3770],{},". وتُبقي ",[18,4083,4084],{},"credentials: 'omit'"," الكوكيز خارج الطلب المرسَل إلى الطرف الثالث الذي ضبطتَه. أمّا شكل الاستجابة فيُتحقّق منه بالتعبير النمطي ",[18,4087,4088],{},"\u002F^[A-Z]{2}$\u002F",", لأنّ بعض واجهات IP تعيد ",[18,4091,4092],{},"\"GB\"",", وبعضها الآخر ",[18,4095,4096],{},"\"United Kingdom\"",", وبعضها ",[18,4099,172],{}," حين لا تعرف. التعبير النمطي هو البوّابة.",[11,4102,4103,4104,4107],{},"يعيش ",[18,4105,4106],{},"detectCountry"," خارج المكوّن عن قصد:",[40,4109,4111],{"className":1893,"code":4110,"language":1895,"meta":45,"style":45},"import { detectCountry } from '@alikhalilll\u002Fui\u002Ftell-input';\n\nconst country = await detectCountry({ strategy: 'locale' });\n",[18,4112,4113,4127,4131],{"__ignoreMap":45},[49,4114,4115,4117,4120,4122,4125],{"class":51,"line":52},[49,4116,301],{"class":63},[49,4118,4119],{"class":55}," { detectCountry } ",[49,4121,313],{"class":63},[49,4123,4124],{"class":70}," '@alikhalilll\u002Fui\u002Ftell-input'",[49,4126,319],{"class":55},[49,4128,4129],{"class":51,"line":82},[49,4130,325],{"emptyLinePlaceholder":324},[49,4132,4133,4135,4137,4139,4141,4143,4146,4149],{"class":51,"line":88},[49,4134,330],{"class":63},[49,4136,2030],{"class":333},[49,4138,343],{"class":63},[49,4140,3555],{"class":63},[49,4142,3350],{"class":262},[49,4144,4145],{"class":55},"({ strategy: ",[49,4147,4148],{"class":70},"'locale'",[49,4150,1927],{"class":55},[11,4152,4153,4154,4157,4158,4161],{},"بلا اعتماد على Vue, ولا refs تفاعلية، ولا ",[18,4155,4156],{},"onMounted",". يعمل داخل server middleware يريد ضبط كوكي، وفي CLI يريد طباعة موقع المستخدم المرجّح، وفي unit test يريد التحكّم بالاستراتيجية. أمّا الغلاف التفاعلي (",[18,4159,4160],{},"useCountryDetection",") فقشرة رقيقة فوقه للحالة داخل المكوّن. سطحان، وتنفيذ واحد.",[11,4163,4164],{},"والسلسلة كلّها قابلة للاستبدال. إن كنتَ تكتشف الموقع الجغرافي في الخادم أصلاً وتمرّره عبر SSR, فلن ترغب في أن يُعيد المكوّن نداء IP:",[40,4166,4168],{"className":42,"code":4167,"language":44,"meta":45,"style":45},"\u003CATellInput\n  v-model:phone=\"phone\"\n  v-model:country=\"country\"\n  :detector=\"(opts) => Promise.resolve(serverDetectedCountry.value)\"\n\u002F>\n",[18,4169,4170,4177,4196,4212,4244],{"__ignoreMap":45},[49,4171,4172,4174],{"class":51,"line":52},[49,4173,56],{"class":55},[49,4175,4176],{"class":59},"ATellInput\n",[49,4178,4179,4182,4184,4187,4189,4191,4193],{"class":51,"line":82},[49,4180,4181],{"class":262},"  v-model",[49,4183,337],{"class":55},[49,4185,4186],{"class":262},"phone",[49,4188,67],{"class":55},[49,4190,71],{"class":70},[49,4192,4186],{"class":55},[49,4194,4195],{"class":70},"\"\n",[49,4197,4198,4200,4202,4204,4206,4208,4210],{"class":51,"line":88},[49,4199,4181],{"class":262},[49,4201,337],{"class":55},[49,4203,1959],{"class":262},[49,4205,67],{"class":55},[49,4207,71],{"class":70},[49,4209,1959],{"class":55},[49,4211,4195],{"class":70},[49,4213,4214,4217,4220,4222,4224,4226,4228,4230,4232,4234,4236,4239,4242],{"class":51,"line":94},[49,4215,4216],{"class":55},"  :",[49,4218,4219],{"class":262},"detector",[49,4221,67],{"class":55},[49,4223,71],{"class":70},[49,4225,625],{"class":55},[49,4227,3355],{"class":601},[49,4229,807],{"class":55},[49,4231,648],{"class":63},[49,4233,3370],{"class":333},[49,4235,527],{"class":55},[49,4237,4238],{"class":262},"resolve",[49,4240,4241],{"class":55},"(serverDetectedCountry.value)",[49,4243,4195],{"class":70},[49,4245,4246],{"class":51,"line":363},[49,4247,4248],{"class":55},"\u002F>\n",[11,4250,4251,4252,4254],{},"يستبدل الـ prop المسمّى ",[18,4253,4219],{}," السلسلةَ المدمجةَ بأكملها. يواصل المكوّن تشغيل بقية منطق الاكتشاف (يضبط الدولة المستنتجة، ويستخدمها تلميحاً للطبقة الثانية، وهكذا)، لكنّ مصدر الإجابة يصبح ما حدّدتَه أنت.",[26,4256,4258],{"id":4257},"منتقٍ-متجاوب-مكوّن-واحد-بتقديمَين","منتقٍ متجاوب: مكوّن واحد بتقديمَين",[11,4260,4261],{},"كانت قائمة الدولة القديمة popover في كلّ مقاسات الشاشة. على الهاتف كان ذلك يعني قائمة قابلة للتمرير من 250 صفّاً مثبّتة على زرّ عرضه 38 بكسل، مع حقل بحث تحجبه لوحة المفاتيح باستمرار. كان يعمل. لكنّه يعمل بشكل رديء.",[11,4263,4264,4265,4268,4269,4272],{},"يعتمد المنتقي الجديد على العنصر الأساسي ",[18,4266,4267],{},"AResponsivePopover"," من المكتبة نفسها، وهو popover على الحاسوب وdrawer عبر ",[18,4270,4271],{},"vaul-vue"," على الجوّال:",[40,4274,4276],{"className":42,"code":4275,"language":44,"meta":45,"style":45},"\u003CAResponsivePopover v-model:open=\"open\">\n  \u003CAResponsivePopoverTrigger as-child>\n    \u003C!-- trigger button -->\n  \u003C\u002FAResponsivePopoverTrigger>\n\n  \u003CAResponsivePopoverContent\n    :popover-class=\"\n      cn(\n        'w-[min(20rem,calc(100vw-2rem))] max-h-[min(22rem,var(--reka-popover-content-available-height))]',\n        props.popoverClass\n      )\n    \"\n    :drawer-class=\"cn('max-h-[80vh] pb-4', props.drawerClass)\"\n  >\n    \u003C!-- search + list -->\n  \u003C\u002FAResponsivePopoverContent>\n\u003C\u002FAResponsivePopover>\n",[18,4277,4278,4302,4307,4312,4317,4321,4326,4331,4336,4341,4346,4351,4356,4361,4366,4371,4376],{"__ignoreMap":45},[49,4279,4280,4282,4284,4287,4289,4292,4294,4296,4298,4300],{"class":51,"line":52},[49,4281,56],{"class":55},[49,4283,4267],{"class":59},[49,4285,4286],{"class":262}," v-model",[49,4288,337],{"class":55},[49,4290,4291],{"class":262},"open",[49,4293,67],{"class":55},[49,4295,71],{"class":70},[49,4297,4291],{"class":55},[49,4299,71],{"class":70},[49,4301,79],{"class":55},[49,4303,4304],{"class":51,"line":82},[49,4305,4306],{"class":55},"  \u003CAResponsivePopoverTrigger as-child>\n",[49,4308,4309],{"class":51,"line":88},[49,4310,4311],{"class":55},"    \u003C!-- trigger button -->\n",[49,4313,4314],{"class":51,"line":94},[49,4315,4316],{"class":55},"  \u003C\u002FAResponsivePopoverTrigger>\n",[49,4318,4319],{"class":51,"line":363},[49,4320,325],{"emptyLinePlaceholder":324},[49,4322,4323],{"class":51,"line":374},[49,4324,4325],{"class":55},"  \u003CAResponsivePopoverContent\n",[49,4327,4328],{"class":51,"line":385},[49,4329,4330],{"class":55},"    :popover-class=\"\n",[49,4332,4333],{"class":51,"line":662},[49,4334,4335],{"class":55},"      cn(\n",[49,4337,4338],{"class":51,"line":691},[49,4339,4340],{"class":55},"        'w-[min(20rem,calc(100vw-2rem))] max-h-[min(22rem,var(--reka-popover-content-available-height))]',\n",[49,4342,4343],{"class":51,"line":696},[49,4344,4345],{"class":55},"        props.popoverClass\n",[49,4347,4348],{"class":51,"line":723},[49,4349,4350],{"class":55},"      )\n",[49,4352,4353],{"class":51,"line":744},[49,4354,4355],{"class":55},"    \"\n",[49,4357,4358],{"class":51,"line":768},[49,4359,4360],{"class":55},"    :drawer-class=\"cn('max-h-[80vh] pb-4', props.drawerClass)\"\n",[49,4362,4363],{"class":51,"line":779},[49,4364,4365],{"class":55},"  >\n",[49,4367,4368],{"class":51,"line":784},[49,4369,4370],{"class":55},"    \u003C!-- search + list -->\n",[49,4372,4373],{"class":51,"line":789},[49,4374,4375],{"class":55},"  \u003C\u002FAResponsivePopoverContent>\n",[49,4377,4378,4380,4382],{"class":51,"line":820},[49,4379,97],{"class":55},[49,4381,4267],{"class":59},[49,4383,79],{"class":55},[11,4385,4386,4387,394,4390,4393,4394,4397,4398,4401,4402,4405],{},"يُمرَّر prop-ان للأصناف (",[18,4388,4389],{},"popoverClass",[18,4391,4392],{},"drawerClass",") كلّ على حدة، فيتمكّن المستهلك من تنسيق كلّ تقديم دون أن يرث الآخر شيئاً منه. الـ popover يريد ",[18,4395,4396],{},"max-h-[min(22rem,var(--reka-popover-content-available-height))]"," لأنّ ",[18,4399,4400],{},"reka-ui"," يُتيح الارتفاع المتاح كمتغيّر CSS. أمّا الـ drawer فيريد ",[18,4403,4404],{},"max-h-[80vh]"," لأنّه ورقة سفلية و\"80% من نافذة العرض\" سقفٌ منطقي. فرض مجموعة أصناف واحدة على الحالتين سيكون خطأً على إحداهما، لكنّ السماح للمستهلك بتجاوز كلّ منهما مستقلاً هو تحديداً نقطة الفصل التي تحتاجها المكتبة.",[11,4407,4408],{},"الفائدة الجانبية الأخرى تخصّ البحث. في وضع الـ drawer يرسو حقل البحث في أعلى ورقة تغطّي معظم الشاشة، ولوحة المفاتيح تدفع الورقة لأعلى بدل أن تدفنها. الشيفرة نفسها، والمكوّن نفسه، والشكل المناسب للجهاز. ليس media-query داخل CSS المستهلك، بل عنصر أساسي يعرف الفرق بنفسه.",[26,4410,4412],{"id":4411},"slots-لكلّ-شيء-أربعة-عشر-slot","Slots لكلّ شيء (أربعة عشر Slot)",[11,4414,4415],{},"كانت النسخة القديمة كتلة واحدة صمّاء. تنسيق رسالة الخطأ يعني ترقيع المكوّن. تغيير أيقونة السهم يعني ترقيع المكوّن. استبدال علَم الدولة بواحد من CDN خاصّ بك يعني ترقيع المكوّن.",[11,4417,4418],{},"النسخة الجديدة تملك السلوك وتترك للمستهلك الشكل:",[40,4420,4422],{"className":1893,"code":4421,"language":1895,"meta":45,"style":45},"defineSlots\u003C{\n  prefix?: () => unknown;\n  suffix?: (props: { validationState: 'idle' | 'valid' | 'error'; validation: PhoneValidationResult }) => unknown;\n  'valid-icon'?: () => unknown;\n  'error-icon'?: (props: { reason: string }) => unknown;\n  hint?: (props: { country: string; formatHint: string; example: string | null }) => unknown;\n  error?: (props: { message: string; reason: string; validation: PhoneValidationResult }) => unknown;\n  trigger?: (props: { selectedCountry: CountryOption | null; open: boolean; sizeClasses: string }) => unknown;\n  chevron?: (props: { open: boolean }) => unknown;\n  flag?: (props: { country: CountryOption; context: 'trigger' | 'item' }) => unknown;\n  item?: (props: { country: CountryOption; selected: boolean; disabled: boolean; select: () => void }) => unknown;\n  'group-header'?: (props: { label: string; group: 'suggested' | 'all' }) => unknown;\n  search?: (props: { value: string; setValue: (v: string) => void; isSearching: boolean }) => unknown;\n  loading?: () => unknown;\n  empty?: (props: { query: string }) => unknown;\n}>();\n",[18,4423,4424,4432,4449,4501,4516,4546,4597,4643,4696,4725,4769,4830,4875,4936,4951,4981],{"__ignoreMap":45},[49,4425,4426,4429],{"class":51,"line":52},[49,4427,4428],{"class":262},"defineSlots",[49,4430,4431],{"class":55},"\u003C{\n",[49,4433,4434,4437,4439,4442,4444,4447],{"class":51,"line":82},[49,4435,4436],{"class":262},"  prefix",[49,4438,3946],{"class":63},[49,4440,4441],{"class":55}," () ",[49,4443,648],{"class":63},[49,4445,4446],{"class":333}," unknown",[49,4448,319],{"class":55},[49,4450,4451,4454,4456,4458,4460,4462,4464,4467,4469,4472,4474,4477,4479,4482,4484,4487,4489,4492,4495,4497,4499],{"class":51,"line":88},[49,4452,4453],{"class":262},"  suffix",[49,4455,3946],{"class":63},[49,4457,2183],{"class":55},[49,4459,732],{"class":601},[49,4461,337],{"class":63},[49,4463,701],{"class":55},[49,4465,4466],{"class":601},"validationState",[49,4468,337],{"class":63},[49,4470,4471],{"class":70}," 'idle'",[49,4473,679],{"class":63},[49,4475,4476],{"class":70}," 'valid'",[49,4478,679],{"class":63},[49,4480,4481],{"class":70}," 'error'",[49,4483,3951],{"class":55},[49,4485,4486],{"class":601},"validation",[49,4488,337],{"class":63},[49,4490,4491],{"class":262}," PhoneValidationResult",[49,4493,4494],{"class":55}," }) ",[49,4496,648],{"class":63},[49,4498,4446],{"class":333},[49,4500,319],{"class":55},[49,4502,4503,4506,4508,4510,4512,4514],{"class":51,"line":94},[49,4504,4505],{"class":70},"  'valid-icon'",[49,4507,3946],{"class":63},[49,4509,4441],{"class":55},[49,4511,648],{"class":63},[49,4513,4446],{"class":333},[49,4515,319],{"class":55},[49,4517,4518,4521,4523,4525,4527,4529,4531,4534,4536,4538,4540,4542,4544],{"class":51,"line":363},[49,4519,4520],{"class":70},"  'error-icon'",[49,4522,3946],{"class":63},[49,4524,2183],{"class":55},[49,4526,732],{"class":601},[49,4528,337],{"class":63},[49,4530,701],{"class":55},[49,4532,4533],{"class":601},"reason",[49,4535,337],{"class":63},[49,4537,607],{"class":333},[49,4539,4494],{"class":55},[49,4541,648],{"class":63},[49,4543,4446],{"class":333},[49,4545,319],{"class":55},[49,4547,4548,4551,4553,4555,4557,4559,4561,4563,4565,4567,4569,4572,4574,4576,4578,4581,4583,4585,4587,4589,4591,4593,4595],{"class":51,"line":374},[49,4549,4550],{"class":262},"  hint",[49,4552,3946],{"class":63},[49,4554,2183],{"class":55},[49,4556,732],{"class":601},[49,4558,337],{"class":63},[49,4560,701],{"class":55},[49,4562,1959],{"class":601},[49,4564,337],{"class":63},[49,4566,607],{"class":333},[49,4568,3951],{"class":55},[49,4570,4571],{"class":601},"formatHint",[49,4573,337],{"class":63},[49,4575,607],{"class":333},[49,4577,3951],{"class":55},[49,4579,4580],{"class":601},"example",[49,4582,337],{"class":63},[49,4584,607],{"class":333},[49,4586,679],{"class":63},[49,4588,657],{"class":333},[49,4590,4494],{"class":55},[49,4592,648],{"class":63},[49,4594,4446],{"class":333},[49,4596,319],{"class":55},[49,4598,4599,4602,4604,4606,4608,4610,4612,4615,4617,4619,4621,4623,4625,4627,4629,4631,4633,4635,4637,4639,4641],{"class":51,"line":385},[49,4600,4601],{"class":262},"  error",[49,4603,3946],{"class":63},[49,4605,2183],{"class":55},[49,4607,732],{"class":601},[49,4609,337],{"class":63},[49,4611,701],{"class":55},[49,4613,4614],{"class":601},"message",[49,4616,337],{"class":63},[49,4618,607],{"class":333},[49,4620,3951],{"class":55},[49,4622,4533],{"class":601},[49,4624,337],{"class":63},[49,4626,607],{"class":333},[49,4628,3951],{"class":55},[49,4630,4486],{"class":601},[49,4632,337],{"class":63},[49,4634,4491],{"class":262},[49,4636,4494],{"class":55},[49,4638,648],{"class":63},[49,4640,4446],{"class":333},[49,4642,319],{"class":55},[49,4644,4645,4648,4650,4652,4654,4656,4658,4661,4663,4666,4668,4670,4672,4674,4676,4679,4681,4684,4686,4688,4690,4692,4694],{"class":51,"line":662},[49,4646,4647],{"class":262},"  trigger",[49,4649,3946],{"class":63},[49,4651,2183],{"class":55},[49,4653,732],{"class":601},[49,4655,337],{"class":63},[49,4657,701],{"class":55},[49,4659,4660],{"class":601},"selectedCountry",[49,4662,337],{"class":63},[49,4664,4665],{"class":262}," CountryOption",[49,4667,679],{"class":63},[49,4669,657],{"class":333},[49,4671,3951],{"class":55},[49,4673,4291],{"class":601},[49,4675,337],{"class":63},[49,4677,4678],{"class":333}," boolean",[49,4680,3951],{"class":55},[49,4682,4683],{"class":601},"sizeClasses",[49,4685,337],{"class":63},[49,4687,607],{"class":333},[49,4689,4494],{"class":55},[49,4691,648],{"class":63},[49,4693,4446],{"class":333},[49,4695,319],{"class":55},[49,4697,4698,4701,4703,4705,4707,4709,4711,4713,4715,4717,4719,4721,4723],{"class":51,"line":691},[49,4699,4700],{"class":262},"  chevron",[49,4702,3946],{"class":63},[49,4704,2183],{"class":55},[49,4706,732],{"class":601},[49,4708,337],{"class":63},[49,4710,701],{"class":55},[49,4712,4291],{"class":601},[49,4714,337],{"class":63},[49,4716,4678],{"class":333},[49,4718,4494],{"class":55},[49,4720,648],{"class":63},[49,4722,4446],{"class":333},[49,4724,319],{"class":55},[49,4726,4727,4730,4732,4734,4736,4738,4740,4742,4744,4746,4748,4751,4753,4756,4758,4761,4763,4765,4767],{"class":51,"line":696},[49,4728,4729],{"class":262},"  flag",[49,4731,3946],{"class":63},[49,4733,2183],{"class":55},[49,4735,732],{"class":601},[49,4737,337],{"class":63},[49,4739,701],{"class":55},[49,4741,1959],{"class":601},[49,4743,337],{"class":63},[49,4745,4665],{"class":262},[49,4747,3951],{"class":55},[49,4749,4750],{"class":601},"context",[49,4752,337],{"class":63},[49,4754,4755],{"class":70}," 'trigger'",[49,4757,679],{"class":63},[49,4759,4760],{"class":70}," 'item'",[49,4762,4494],{"class":55},[49,4764,648],{"class":63},[49,4766,4446],{"class":333},[49,4768,319],{"class":55},[49,4770,4771,4774,4776,4778,4780,4782,4784,4786,4788,4790,4792,4795,4797,4799,4801,4804,4806,4808,4810,4813,4815,4817,4819,4822,4824,4826,4828],{"class":51,"line":723},[49,4772,4773],{"class":262},"  item",[49,4775,3946],{"class":63},[49,4777,2183],{"class":55},[49,4779,732],{"class":601},[49,4781,337],{"class":63},[49,4783,701],{"class":55},[49,4785,1959],{"class":601},[49,4787,337],{"class":63},[49,4789,4665],{"class":262},[49,4791,3951],{"class":55},[49,4793,4794],{"class":601},"selected",[49,4796,337],{"class":63},[49,4798,4678],{"class":333},[49,4800,3951],{"class":55},[49,4802,4803],{"class":601},"disabled",[49,4805,337],{"class":63},[49,4807,4678],{"class":333},[49,4809,3951],{"class":55},[49,4811,4812],{"class":262},"select",[49,4814,337],{"class":63},[49,4816,4441],{"class":55},[49,4818,648],{"class":63},[49,4820,4821],{"class":333}," void",[49,4823,4494],{"class":55},[49,4825,648],{"class":63},[49,4827,4446],{"class":333},[49,4829,319],{"class":55},[49,4831,4832,4835,4837,4839,4841,4843,4845,4848,4850,4852,4854,4857,4859,4862,4864,4867,4869,4871,4873],{"class":51,"line":744},[49,4833,4834],{"class":70},"  'group-header'",[49,4836,3946],{"class":63},[49,4838,2183],{"class":55},[49,4840,732],{"class":601},[49,4842,337],{"class":63},[49,4844,701],{"class":55},[49,4846,4847],{"class":601},"label",[49,4849,337],{"class":63},[49,4851,607],{"class":333},[49,4853,3951],{"class":55},[49,4855,4856],{"class":601},"group",[49,4858,337],{"class":63},[49,4860,4861],{"class":70}," 'suggested'",[49,4863,679],{"class":63},[49,4865,4866],{"class":70}," 'all'",[49,4868,4494],{"class":55},[49,4870,648],{"class":63},[49,4872,4446],{"class":333},[49,4874,319],{"class":55},[49,4876,4877,4880,4882,4884,4886,4888,4890,4893,4895,4897,4899,4902,4904,4906,4909,4911,4913,4915,4917,4919,4921,4924,4926,4928,4930,4932,4934],{"class":51,"line":768},[49,4878,4879],{"class":262},"  search",[49,4881,3946],{"class":63},[49,4883,2183],{"class":55},[49,4885,732],{"class":601},[49,4887,337],{"class":63},[49,4889,701],{"class":55},[49,4891,4892],{"class":601},"value",[49,4894,337],{"class":63},[49,4896,607],{"class":333},[49,4898,3951],{"class":55},[49,4900,4901],{"class":262},"setValue",[49,4903,337],{"class":63},[49,4905,2183],{"class":55},[49,4907,4908],{"class":601},"v",[49,4910,337],{"class":63},[49,4912,607],{"class":333},[49,4914,807],{"class":55},[49,4916,648],{"class":63},[49,4918,4821],{"class":333},[49,4920,3951],{"class":55},[49,4922,4923],{"class":601},"isSearching",[49,4925,337],{"class":63},[49,4927,4678],{"class":333},[49,4929,4494],{"class":55},[49,4931,648],{"class":63},[49,4933,4446],{"class":333},[49,4935,319],{"class":55},[49,4937,4938,4941,4943,4945,4947,4949],{"class":51,"line":779},[49,4939,4940],{"class":262},"  loading",[49,4942,3946],{"class":63},[49,4944,4441],{"class":55},[49,4946,648],{"class":63},[49,4948,4446],{"class":333},[49,4950,319],{"class":55},[49,4952,4953,4956,4958,4960,4962,4964,4966,4969,4971,4973,4975,4977,4979],{"class":51,"line":784},[49,4954,4955],{"class":262},"  empty",[49,4957,3946],{"class":63},[49,4959,2183],{"class":55},[49,4961,732],{"class":601},[49,4963,337],{"class":63},[49,4965,701],{"class":55},[49,4967,4968],{"class":601},"query",[49,4970,337],{"class":63},[49,4972,607],{"class":333},[49,4974,4494],{"class":55},[49,4976,648],{"class":63},[49,4978,4446],{"class":333},[49,4980,319],{"class":55},[49,4982,4983],{"class":51,"line":789},[49,4984,4985],{"class":55},"}>();\n",[11,4987,4988,4989,159,4992,4995,4996,3705,4999,5002,5003,159,5006,159,5009,5012,5013,159,5016,159,5018,159,5021,159,5024,5027],{},"أربعة عشر slot موزّعة على ثلاث مجموعات. المجموعة البصرية (",[18,4990,4991],{},"prefix",[18,4993,4994],{},"suffix",", وslot-ا الأيقونتين، و",[18,4997,4998],{},"hint",[18,5000,5001],{},"error",") تزيّن الحقل نفسه. مجموعة مُشغِّل المنتقي (",[18,5004,5005],{},"trigger",[18,5007,5008],{},"chevron",[18,5010,5011],{},"flag",") تستبدل الزرّ الذي ينقر عليه المستخدم. ومجموعة محتوى المنتقي (",[18,5014,5015],{},"search",[18,5017,263],{},[18,5019,5020],{},"empty",[18,5022,5023],{},"group-header",[18,5025,5026],{},"item",") تستبدل أيّ شيء داخل الـ popover أو الـ drawer.",[11,5029,5030,5031,5034],{},"معظم slot-ات المنتقي تُمرَّر مباشرة إلى ",[18,5032,5033],{},"ACountrySelect",". المكوّن نفسه يتولّى التوجيه:",[40,5036,5038],{"className":42,"code":5037,"language":44,"meta":45,"style":45},"\u003Ctemplate v-if=\"$slots.trigger\" #trigger=\"slotProps\">\n  \u003Cslot name=\"trigger\" v-bind=\"slotProps\" \u002F>\n\u003C\u002Ftemplate>\n\u003Ctemplate v-if=\"$slots.chevron\" #chevron=\"slotProps\">\n  \u003Cslot name=\"chevron\" v-bind=\"slotProps\" \u002F>\n\u003C\u002Ftemplate>\n\u003C!-- ... and so on for each forwarded slot -->\n",[18,5039,5040,5073,5100,5108,5139,5162,5170],{"__ignoreMap":45},[49,5041,5042,5044,5046,5048,5050,5052,5055,5057,5060,5062,5064,5066,5069,5071],{"class":51,"line":52},[49,5043,56],{"class":55},[49,5045,839],{"class":59},[49,5047,64],{"class":63},[49,5049,67],{"class":55},[49,5051,71],{"class":70},[49,5053,5054],{"class":55},"$slots.trigger",[49,5056,71],{"class":70},[49,5058,5059],{"class":55}," #",[49,5061,5005],{"class":262},[49,5063,67],{"class":55},[49,5065,71],{"class":70},[49,5067,5068],{"class":55},"slotProps",[49,5070,71],{"class":70},[49,5072,79],{"class":55},[49,5074,5075,5077,5080,5083,5085,5088,5091,5093,5096,5098],{"class":51,"line":82},[49,5076,847],{"class":55},[49,5078,5079],{"class":59},"slot",[49,5081,5082],{"class":262}," name",[49,5084,67],{"class":55},[49,5086,5087],{"class":70},"\"trigger\"",[49,5089,5090],{"class":262}," v-bind",[49,5092,67],{"class":55},[49,5094,5095],{"class":70},"\"slotProps\"",[49,5097,1117],{"class":1116},[49,5099,79],{"class":55},[49,5101,5102,5104,5106],{"class":51,"line":88},[49,5103,97],{"class":55},[49,5105,839],{"class":59},[49,5107,79],{"class":55},[49,5109,5110,5112,5114,5116,5118,5120,5123,5125,5127,5129,5131,5133,5135,5137],{"class":51,"line":94},[49,5111,56],{"class":55},[49,5113,839],{"class":59},[49,5115,64],{"class":63},[49,5117,67],{"class":55},[49,5119,71],{"class":70},[49,5121,5122],{"class":55},"$slots.chevron",[49,5124,71],{"class":70},[49,5126,5059],{"class":55},[49,5128,5008],{"class":262},[49,5130,67],{"class":55},[49,5132,71],{"class":70},[49,5134,5068],{"class":55},[49,5136,71],{"class":70},[49,5138,79],{"class":55},[49,5140,5141,5143,5145,5147,5149,5152,5154,5156,5158,5160],{"class":51,"line":363},[49,5142,847],{"class":55},[49,5144,5079],{"class":59},[49,5146,5082],{"class":262},[49,5148,67],{"class":55},[49,5150,5151],{"class":70},"\"chevron\"",[49,5153,5090],{"class":262},[49,5155,67],{"class":55},[49,5157,5095],{"class":70},[49,5159,1117],{"class":1116},[49,5161,79],{"class":55},[49,5163,5164,5166,5168],{"class":51,"line":374},[49,5165,97],{"class":55},[49,5167,839],{"class":59},[49,5169,79],{"class":55},[49,5171,5172],{"class":51,"line":385},[49,5173,5174],{"class":2472},"\u003C!-- ... and so on for each forwarded slot -->\n",[11,5176,5177,5178,5181,5182,5185,5186,5188],{},"الشرط ",[18,5179,5180],{},"v-if=\"$slots.x\""," هنا مهم. بدونه سيبدو الـ ",[18,5183,5184],{},"\u003Ctemplate #x>"," المُمرَّر موجوداً دائماً من منظور الأب، فلا يحصل الرسم الاحتياطي داخل ",[18,5187,5033],{}," على أيّ فرصة للعمل. الحارس يقول ببساطة: \"لا تُمرّر إلا إذا قدّم المستهلك هذا الـ slot فعلاً\".",[11,5190,5191],{},"مثال تخصيص واقعي:",[40,5193,5195],{"className":42,"code":5194,"language":44,"meta":45,"style":45},"\u003CATellInput v-model:phone=\"phone\" v-model:country=\"country\" show-validation>\n  \u003Ctemplate #suffix=\"{ validationState }\">\n    \u003CSparkle v-if=\"validationState === 'valid'\" class=\"size-4 text-amber-400\" \u002F>\n  \u003C\u002Ftemplate>\n  \u003Ctemplate #error=\"{ message, reason }\">\n    \u003Cp class=\"text-destructive text-xs\">\n      \u003Ccode class=\"bg-muted rounded px-1 py-0.5\">{{ reason }}\u003C\u002Fcode> {{ message }}\n    \u003C\u002Fp>\n  \u003C\u002Ftemplate>\n  \u003Ctemplate #empty=\"{ query }\">\n    \u003Cdiv class=\"px-3 py-6 text-center text-sm\">\n      Nothing matched \"\u003Cstrong>{{ query }}\u003C\u002Fstrong>\".\n      \u003Cbutton type=\"button\" class=\"underline\" @click=\"suggestCountry(query)\">\n        Suggest a country\n      \u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  \u003C\u002Ftemplate>\n\u003C\u002FATellInput>\n",[18,5196,5197,5236,5241,5246,5251,5256,5261,5266,5271,5275,5280,5285,5290,5295,5300,5305,5310,5314],{"__ignoreMap":45},[49,5198,5199,5201,5203,5205,5207,5209,5211,5213,5215,5217,5219,5221,5223,5225,5227,5229,5231,5234],{"class":51,"line":52},[49,5200,56],{"class":55},[49,5202,1865],{"class":59},[49,5204,4286],{"class":262},[49,5206,337],{"class":55},[49,5208,4186],{"class":262},[49,5210,67],{"class":55},[49,5212,71],{"class":70},[49,5214,4186],{"class":55},[49,5216,71],{"class":70},[49,5218,4286],{"class":262},[49,5220,337],{"class":55},[49,5222,1959],{"class":262},[49,5224,67],{"class":55},[49,5226,71],{"class":70},[49,5228,1959],{"class":55},[49,5230,71],{"class":70},[49,5232,5233],{"class":262}," show-validation",[49,5235,79],{"class":55},[49,5237,5238],{"class":51,"line":82},[49,5239,5240],{"class":55},"  \u003Ctemplate #suffix=\"{ validationState }\">\n",[49,5242,5243],{"class":51,"line":88},[49,5244,5245],{"class":55},"    \u003CSparkle v-if=\"validationState === 'valid'\" class=\"size-4 text-amber-400\" \u002F>\n",[49,5247,5248],{"class":51,"line":94},[49,5249,5250],{"class":55},"  \u003C\u002Ftemplate>\n",[49,5252,5253],{"class":51,"line":363},[49,5254,5255],{"class":55},"  \u003Ctemplate #error=\"{ message, reason }\">\n",[49,5257,5258],{"class":51,"line":374},[49,5259,5260],{"class":55},"    \u003Cp class=\"text-destructive text-xs\">\n",[49,5262,5263],{"class":51,"line":385},[49,5264,5265],{"class":55},"      \u003Ccode class=\"bg-muted rounded px-1 py-0.5\">{{ reason }}\u003C\u002Fcode> {{ message }}\n",[49,5267,5268],{"class":51,"line":662},[49,5269,5270],{"class":55},"    \u003C\u002Fp>\n",[49,5272,5273],{"class":51,"line":691},[49,5274,5250],{"class":55},[49,5276,5277],{"class":51,"line":696},[49,5278,5279],{"class":55},"  \u003Ctemplate #empty=\"{ query }\">\n",[49,5281,5282],{"class":51,"line":723},[49,5283,5284],{"class":55},"    \u003Cdiv class=\"px-3 py-6 text-center text-sm\">\n",[49,5286,5287],{"class":51,"line":744},[49,5288,5289],{"class":55},"      Nothing matched \"\u003Cstrong>{{ query }}\u003C\u002Fstrong>\".\n",[49,5291,5292],{"class":51,"line":768},[49,5293,5294],{"class":55},"      \u003Cbutton type=\"button\" class=\"underline\" @click=\"suggestCountry(query)\">\n",[49,5296,5297],{"class":51,"line":779},[49,5298,5299],{"class":55},"        Suggest a country\n",[49,5301,5302],{"class":51,"line":784},[49,5303,5304],{"class":55},"      \u003C\u002Fbutton>\n",[49,5306,5307],{"class":51,"line":789},[49,5308,5309],{"class":55},"    \u003C\u002Fdiv>\n",[49,5311,5312],{"class":51,"line":820},[49,5313,5250],{"class":55},[49,5315,5316,5318,5320],{"class":51,"line":829},[49,5317,97],{"class":55},[49,5319,1865],{"class":59},[49,5321,79],{"class":55},[11,5323,5324],{},"وظيفة مكوّن المكتبة أن يملك السلوك (التحليل، والتحقّق، والاكتشاف، وإدارة الحالة)، وأن يترك للمستهلك التحكّم بشكل هذه الأشياء. أربعة عشر slot هي كلفة هذه الصفقة، وهي كلفة تستحقّ.",[11,5326,5327],{},"المبدأ نفسه ينطبق على الـ props غير البصرية:",[40,5329,5331],{"className":1893,"code":5330,"language":1895,"meta":45,"style":45},"flagUrl?: (iso2: string, width: number) => string;\nsearcher?: (query: string, country: CountryOption) => boolean;\ncountries?: CountryOption[];\ndetector?: (options: DetectCountryOptions) => Promise\u003Cstring | null | undefined>;\nerrorMessages?: Record\u003CPhoneValidationReason, string>;\n",[18,5332,5333,5365,5395,5405,5446],{"__ignoreMap":45},[49,5334,5335,5338,5340,5342,5345,5347,5349,5351,5354,5356,5358,5360,5362],{"class":51,"line":52},[49,5336,5337],{"class":55},"flagUrl",[49,5339,3946],{"class":63},[49,5341,2183],{"class":55},[49,5343,5344],{"class":601},"iso2",[49,5346,337],{"class":63},[49,5348,607],{"class":333},[49,5350,159],{"class":55},[49,5352,5353],{"class":601},"width",[49,5355,337],{"class":63},[49,5357,3775],{"class":333},[49,5359,807],{"class":55},[49,5361,648],{"class":63},[49,5363,5364],{"class":55}," string;\n",[49,5366,5367,5370,5372,5374,5376,5378,5380,5382,5384,5386,5388,5390,5392],{"class":51,"line":82},[49,5368,5369],{"class":55},"searcher",[49,5371,3946],{"class":63},[49,5373,2183],{"class":55},[49,5375,4968],{"class":601},[49,5377,337],{"class":63},[49,5379,607],{"class":333},[49,5381,159],{"class":55},[49,5383,1959],{"class":601},[49,5385,337],{"class":63},[49,5387,4665],{"class":262},[49,5389,807],{"class":55},[49,5391,648],{"class":63},[49,5393,5394],{"class":55}," boolean;\n",[49,5396,5397,5400,5402],{"class":51,"line":88},[49,5398,5399],{"class":55},"countries",[49,5401,3946],{"class":63},[49,5403,5404],{"class":55}," CountryOption[];\n",[49,5406,5407,5409,5411,5413,5416,5418,5420,5422,5424,5426,5428,5431,5434,5436,5438,5441,5444],{"class":51,"line":94},[49,5408,4219],{"class":55},[49,5410,3946],{"class":63},[49,5412,2183],{"class":55},[49,5414,5415],{"class":601},"options",[49,5417,337],{"class":63},[49,5419,3360],{"class":262},[49,5421,807],{"class":55},[49,5423,648],{"class":63},[49,5425,3370],{"class":333},[49,5427,56],{"class":63},[49,5429,5430],{"class":55},"string ",[49,5432,5433],{"class":63},"|",[49,5435,657],{"class":333},[49,5437,679],{"class":63},[49,5439,5440],{"class":333}," undefined",[49,5442,5443],{"class":63},">",[49,5445,319],{"class":55},[49,5447,5448,5451,5453,5456,5458,5461,5463],{"class":51,"line":363},[49,5449,5450],{"class":55},"errorMessages",[49,5452,3946],{"class":63},[49,5454,5455],{"class":55}," Record",[49,5457,56],{"class":63},[49,5459,5460],{"class":55},"PhoneValidationReason, string",[49,5462,5443],{"class":63},[49,5464,319],{"class":55},[11,5466,5467,5468,5470,5471,5474,5475,5477,5478,5480,5481,5484,5485,5487],{},"يُتيح لك ",[18,5469,5337],{}," استبدال ",[18,5472,5473],{},"flagcdn.com"," بـ CDN خاصّ بك، وهو مفيد إن كان لديك CSP لا يسمح بصور من طرف ثالث. ويُتيح لك ",[18,5476,5369],{}," استبدال مطابقة السلاسل الفرعية بمطابقة بادئة، أو مطابقة ضبابية، أو مطابقة بالاسم العربي، أو أيّ شيء يحتاجه مستخدموك فعلاً. ويُتيح لك ",[18,5479,5399],{}," شحن قائمتك المُنتقاة والاستغناء كلياً عن نداء ",[18,5482,5483],{},"REST Countries",", وهو مفيد إن كنتَ حسمتَ أنّك تخدم دول الخليج مثلاً. أمّا ",[18,5486,5450],{}," فمعنيّ بالتوطين: مرّر جدولاً عربياً وستُعرض أسباب التحقّق بالعربية.",[11,5489,5490],{},"كلّ prop له افتراضي منطقي. النداء الذي لا يحتاج تكويناً ما زال يعمل كما هو.",[26,5492,5494],{"id":5493},"جدول-رموز-الاتصال-المتزامن-ولماذا-ليس-اختيارياً","جدول رموز الاتصال المتزامن، ولماذا ليس اختيارياً",[11,5496,5497,5498,5500,5501,2183,5504,159,5507,5510,5511,5514,5515,5518,5519,1260,5521,5523],{},"عند أوّل تركيب للمكوّن في تبويب جديد، لا يكون نداء ",[18,5499,5483],{}," قد اكتمل. أيّ فهارس يبنيها ",[18,5502,5503],{},"usePhoneValidation",[18,5505,5506],{},"byValue",[18,5508,5509],{},"byDialDigits",") تكون خرائط فارغة. لو كتب المستهلك ",[18,5512,5513],{},"default-country=\"20\""," في قالبه متوقّعاً أن يظهر المنتقي وقد اختار مصر، فما سيحصل عليه بلا مساعدة هو منتقٍ فارغ. فهرس أرقام الاتصال يعيد ",[18,5516,5517],{},"[]",", ويضبط الـ watcher قيمة ",[18,5520,2100],{},[18,5522,2021],{},", ولن تظهر مصر إلا حين يعود نداء الشبكة.",[11,5525,5526],{},"لذلك يُدرِج المكوّن جدولاً احتياطياً متزامناً في داخله:",[40,5528,5530],{"className":1893,"code":5529,"language":1895,"meta":45,"style":45},"const DIAL_TO_ISO2_FALLBACK: Record\u003Cstring, string> = {\n  '1': 'US',\n  '7': 'RU',\n  '20': 'EG',\n  '27': 'ZA',\n  '33': 'FR',\n  '44': 'GB',\n  '49': 'DE',\n  '52': 'MX',\n  '55': 'BR',\n  '61': 'AU',\n  '81': 'JP',\n  '86': 'CN',\n  '91': 'IN',\n  '212': 'MA',\n  '966': 'SA',\n  '971': 'AE',\n  \u002F\u002F ... ~50 entries\n};\n",[18,5531,5532,5558,5570,5582,5594,5606,5618,5630,5642,5654,5666,5678,5690,5702,5714,5726,5738,5750,5755],{"__ignoreMap":45},[49,5533,5534,5536,5539,5541,5543,5545,5547,5549,5551,5554,5556],{"class":51,"line":52},[49,5535,330],{"class":63},[49,5537,5538],{"class":333}," DIAL_TO_ISO2_FALLBACK",[49,5540,337],{"class":63},[49,5542,5455],{"class":262},[49,5544,56],{"class":55},[49,5546,1914],{"class":333},[49,5548,159],{"class":55},[49,5550,1914],{"class":333},[49,5552,5553],{"class":55},"> ",[49,5555,67],{"class":63},[49,5557,1625],{"class":55},[49,5559,5560,5563,5565,5568],{"class":51,"line":82},[49,5561,5562],{"class":70},"  '1'",[49,5564,1633],{"class":55},[49,5566,5567],{"class":70},"'US'",[49,5569,360],{"class":55},[49,5571,5572,5575,5577,5580],{"class":51,"line":88},[49,5573,5574],{"class":70},"  '7'",[49,5576,1633],{"class":55},[49,5578,5579],{"class":70},"'RU'",[49,5581,360],{"class":55},[49,5583,5584,5587,5589,5592],{"class":51,"line":94},[49,5585,5586],{"class":70},"  '20'",[49,5588,1633],{"class":55},[49,5590,5591],{"class":70},"'EG'",[49,5593,360],{"class":55},[49,5595,5596,5599,5601,5604],{"class":51,"line":363},[49,5597,5598],{"class":70},"  '27'",[49,5600,1633],{"class":55},[49,5602,5603],{"class":70},"'ZA'",[49,5605,360],{"class":55},[49,5607,5608,5611,5613,5616],{"class":51,"line":374},[49,5609,5610],{"class":70},"  '33'",[49,5612,1633],{"class":55},[49,5614,5615],{"class":70},"'FR'",[49,5617,360],{"class":55},[49,5619,5620,5623,5625,5628],{"class":51,"line":385},[49,5621,5622],{"class":70},"  '44'",[49,5624,1633],{"class":55},[49,5626,5627],{"class":70},"'GB'",[49,5629,360],{"class":55},[49,5631,5632,5635,5637,5640],{"class":51,"line":662},[49,5633,5634],{"class":70},"  '49'",[49,5636,1633],{"class":55},[49,5638,5639],{"class":70},"'DE'",[49,5641,360],{"class":55},[49,5643,5644,5647,5649,5652],{"class":51,"line":691},[49,5645,5646],{"class":70},"  '52'",[49,5648,1633],{"class":55},[49,5650,5651],{"class":70},"'MX'",[49,5653,360],{"class":55},[49,5655,5656,5659,5661,5664],{"class":51,"line":696},[49,5657,5658],{"class":70},"  '55'",[49,5660,1633],{"class":55},[49,5662,5663],{"class":70},"'BR'",[49,5665,360],{"class":55},[49,5667,5668,5671,5673,5676],{"class":51,"line":723},[49,5669,5670],{"class":70},"  '61'",[49,5672,1633],{"class":55},[49,5674,5675],{"class":70},"'AU'",[49,5677,360],{"class":55},[49,5679,5680,5683,5685,5688],{"class":51,"line":744},[49,5681,5682],{"class":70},"  '81'",[49,5684,1633],{"class":55},[49,5686,5687],{"class":70},"'JP'",[49,5689,360],{"class":55},[49,5691,5692,5695,5697,5700],{"class":51,"line":768},[49,5693,5694],{"class":70},"  '86'",[49,5696,1633],{"class":55},[49,5698,5699],{"class":70},"'CN'",[49,5701,360],{"class":55},[49,5703,5704,5707,5709,5712],{"class":51,"line":779},[49,5705,5706],{"class":70},"  '91'",[49,5708,1633],{"class":55},[49,5710,5711],{"class":70},"'IN'",[49,5713,360],{"class":55},[49,5715,5716,5719,5721,5724],{"class":51,"line":784},[49,5717,5718],{"class":70},"  '212'",[49,5720,1633],{"class":55},[49,5722,5723],{"class":70},"'MA'",[49,5725,360],{"class":55},[49,5727,5728,5731,5733,5736],{"class":51,"line":789},[49,5729,5730],{"class":70},"  '966'",[49,5732,1633],{"class":55},[49,5734,5735],{"class":70},"'SA'",[49,5737,360],{"class":55},[49,5739,5740,5743,5745,5748],{"class":51,"line":820},[49,5741,5742],{"class":70},"  '971'",[49,5744,1633],{"class":55},[49,5746,5747],{"class":70},"'AE'",[49,5749,360],{"class":55},[49,5751,5752],{"class":51,"line":829},[49,5753,5754],{"class":2472},"  \u002F\u002F ... ~50 entries\n",[49,5756,5757],{"class":51,"line":834},[49,5758,5759],{"class":55},"};\n",[11,5761,5762,5763,5766],{},"ويتحقّق ",[18,5764,5765],{},"resolveCountryIdentifier"," أولاً من الفهرس المحمَّل، ثم من هذا الجدول:",[40,5768,5770],{"className":1893,"code":5769,"language":1895,"meta":45,"style":45},"function resolveCountryIdentifier(raw: string | undefined | null): string {\n  const v = String(raw ?? '').trim();\n  if (!v) return '';\n  if (\u002F^[A-Za-z]{2}$\u002F.test(v)) return v.toUpperCase();\n  const dial = v.replace(\u002F^\\+\u002F, '');\n  if (!\u002F^\\d+$\u002F.test(dial)) return '';\n  const match = getCountriesByDial(dial)[0];\n  if (match) return match.value;\n  return DIAL_TO_ISO2_FALLBACK[dial] ?? '';\n}\n",[18,5771,5772,5803,5829,5846,5879,5911,5944,5962,5974,5989],{"__ignoreMap":45},[49,5773,5774,5776,5778,5780,5783,5785,5787,5789,5791,5793,5795,5797,5799,5801],{"class":51,"line":52},[49,5775,2418],{"class":63},[49,5777,2212],{"class":262},[49,5779,625],{"class":55},[49,5781,5782],{"class":601},"raw",[49,5784,337],{"class":63},[49,5786,607],{"class":333},[49,5788,679],{"class":63},[49,5790,5440],{"class":333},[49,5792,679],{"class":63},[49,5794,657],{"class":333},[49,5796,2433],{"class":55},[49,5798,337],{"class":63},[49,5800,607],{"class":333},[49,5802,1625],{"class":55},[49,5804,5805,5807,5810,5812,5815,5818,5820,5822,5824,5827],{"class":51,"line":82},[49,5806,2603],{"class":63},[49,5808,5809],{"class":333}," v",[49,5811,343],{"class":63},[49,5813,5814],{"class":262}," String",[49,5816,5817],{"class":55},"(raw ",[49,5819,2555],{"class":63},[49,5821,2162],{"class":70},[49,5823,1251],{"class":55},[49,5825,5826],{"class":262},"trim",[49,5828,3477],{"class":55},[49,5830,5831,5833,5835,5837,5840,5842,5844],{"class":51,"line":88},[49,5832,2449],{"class":63},[49,5834,2183],{"class":55},[49,5836,2454],{"class":63},[49,5838,5839],{"class":55},"v) ",[49,5841,2197],{"class":63},[49,5843,2162],{"class":70},[49,5845,319],{"class":55},[49,5847,5848,5850,5852,5854,5856,5859,5861,5863,5865,5867,5870,5872,5875,5877],{"class":51,"line":94},[49,5849,2449],{"class":63},[49,5851,2183],{"class":55},[49,5853,4011],{"class":70},[49,5855,4002],{"class":63},[49,5857,5858],{"class":333},"[A-Za-z]",[49,5860,4008],{"class":63},[49,5862,4011],{"class":70},[49,5864,527],{"class":55},[49,5866,4016],{"class":262},[49,5868,5869],{"class":55},"(v)) ",[49,5871,2197],{"class":63},[49,5873,5874],{"class":55}," v.",[49,5876,3521],{"class":262},[49,5878,3477],{"class":55},[49,5880,5881,5883,5886,5888,5890,5893,5895,5897,5899,5903,5905,5907,5909],{"class":51,"line":363},[49,5882,2603],{"class":63},[49,5884,5885],{"class":333}," dial",[49,5887,343],{"class":63},[49,5889,5874],{"class":55},[49,5891,5892],{"class":262},"replace",[49,5894,625],{"class":55},[49,5896,4011],{"class":70},[49,5898,4002],{"class":63},[49,5900,5902],{"class":5901},"snhLl","\\+",[49,5904,4011],{"class":70},[49,5906,159],{"class":55},[49,5908,2021],{"class":70},[49,5910,630],{"class":55},[49,5912,5913,5915,5917,5919,5921,5923,5926,5929,5931,5933,5935,5938,5940,5942],{"class":51,"line":374},[49,5914,2449],{"class":63},[49,5916,2183],{"class":55},[49,5918,2454],{"class":63},[49,5920,4011],{"class":70},[49,5922,4002],{"class":63},[49,5924,5925],{"class":333},"\\d",[49,5927,5928],{"class":63},"+$",[49,5930,4011],{"class":70},[49,5932,527],{"class":55},[49,5934,4016],{"class":262},[49,5936,5937],{"class":55},"(dial)) ",[49,5939,2197],{"class":63},[49,5941,2162],{"class":70},[49,5943,319],{"class":55},[49,5945,5946,5948,5950,5952,5954,5957,5959],{"class":51,"line":385},[49,5947,2603],{"class":63},[49,5949,3142],{"class":333},[49,5951,343],{"class":63},[49,5953,2855],{"class":262},[49,5955,5956],{"class":55},"(dial)[",[49,5958,2839],{"class":333},[49,5960,5961],{"class":55},"];\n",[49,5963,5964,5966,5969,5971],{"class":51,"line":662},[49,5965,2449],{"class":63},[49,5967,5968],{"class":55}," (match) ",[49,5970,2197],{"class":63},[49,5972,5973],{"class":55}," match.value;\n",[49,5975,5976,5978,5980,5983,5985,5987],{"class":51,"line":691},[49,5977,2962],{"class":63},[49,5979,5538],{"class":333},[49,5981,5982],{"class":55},"[dial] ",[49,5984,2555],{"class":63},[49,5986,2162],{"class":70},[49,5988,319],{"class":55},[49,5990,5991],{"class":51,"line":696},[49,5992,1719],{"class":55},[11,5994,5995],{},"الاتجاه المعاكس، أي التحويل من ISO2 إلى رقم اتصال من أجل v-model, يُجري الفحص نفسه:",[40,5997,5999],{"className":1893,"code":5998,"language":1895,"meta":45,"style":45},"function dialNumberFor(iso2: string): number | null {\n  if (!iso2) return null;\n  const fromIndex = getCountryByValue(iso2)?.raw_data?.dial_digits;\n  const digits = fromIndex ?? Object.entries(DIAL_TO_ISO2_FALLBACK).find(([, v]) => v === iso2)?.[0];\n  if (!digits) return null;\n  const n = Number(digits);\n  return Number.isFinite(n) ? n : null;\n}\n",[18,6000,6001,6028,6045,6059,6111,6127,6142,6166],{"__ignoreMap":45},[49,6002,6003,6005,6008,6010,6012,6014,6016,6018,6020,6022,6024,6026],{"class":51,"line":52},[49,6004,2418],{"class":63},[49,6006,6007],{"class":262}," dialNumberFor",[49,6009,625],{"class":55},[49,6011,5344],{"class":601},[49,6013,337],{"class":63},[49,6015,607],{"class":333},[49,6017,2433],{"class":55},[49,6019,337],{"class":63},[49,6021,3775],{"class":333},[49,6023,679],{"class":63},[49,6025,657],{"class":333},[49,6027,1625],{"class":55},[49,6029,6030,6032,6034,6036,6039,6041,6043],{"class":51,"line":82},[49,6031,2449],{"class":63},[49,6033,2183],{"class":55},[49,6035,2454],{"class":63},[49,6037,6038],{"class":55},"iso2) ",[49,6040,2197],{"class":63},[49,6042,657],{"class":333},[49,6044,319],{"class":55},[49,6046,6047,6049,6052,6054,6056],{"class":51,"line":88},[49,6048,2603],{"class":63},[49,6050,6051],{"class":333}," fromIndex",[49,6053,343],{"class":63},[49,6055,2529],{"class":262},[49,6057,6058],{"class":55},"(iso2)?.raw_data?.dial_digits;\n",[49,6060,6061,6063,6066,6068,6071,6073,6076,6079,6081,6084,6086,6089,6092,6094,6097,6099,6102,6104,6107,6109],{"class":51,"line":94},[49,6062,2603],{"class":63},[49,6064,6065],{"class":333}," digits",[49,6067,343],{"class":63},[49,6069,6070],{"class":55}," fromIndex ",[49,6072,2555],{"class":63},[49,6074,6075],{"class":55}," Object.",[49,6077,6078],{"class":262},"entries",[49,6080,625],{"class":55},[49,6082,6083],{"class":333},"DIAL_TO_ISO2_FALLBACK",[49,6085,1251],{"class":55},[49,6087,6088],{"class":262},"find",[49,6090,6091],{"class":55},"(([, ",[49,6093,4908],{"class":601},[49,6095,6096],{"class":55},"]) ",[49,6098,648],{"class":63},[49,6100,6101],{"class":55}," v ",[49,6103,654],{"class":63},[49,6105,6106],{"class":55}," iso2)?.[",[49,6108,2839],{"class":333},[49,6110,5961],{"class":55},[49,6112,6113,6115,6117,6119,6121,6123,6125],{"class":51,"line":363},[49,6114,2449],{"class":63},[49,6116,2183],{"class":55},[49,6118,2454],{"class":63},[49,6120,2457],{"class":55},[49,6122,2197],{"class":63},[49,6124,657],{"class":333},[49,6126,319],{"class":55},[49,6128,6129,6131,6134,6136,6139],{"class":51,"line":374},[49,6130,2603],{"class":63},[49,6132,6133],{"class":333}," n",[49,6135,343],{"class":63},[49,6137,6138],{"class":262}," Number",[49,6140,6141],{"class":55},"(digits);\n",[49,6143,6144,6146,6149,6152,6155,6157,6160,6162,6164],{"class":51,"line":385},[49,6145,2962],{"class":63},[49,6147,6148],{"class":55}," Number.",[49,6150,6151],{"class":262},"isFinite",[49,6153,6154],{"class":55},"(n) ",[49,6156,758],{"class":63},[49,6158,6159],{"class":55}," n ",[49,6161,337],{"class":63},[49,6163,657],{"class":333},[49,6165,319],{"class":55},[49,6167,6168],{"class":51,"line":662},[49,6169,1719],{"class":55},[11,6171,6172,6173,6176],{},"الجدول تسوية معلومة سلفاً. خمسون مدخلاً لا تكفي لتغطية كلّ رمز اتصال، واختيار الخمسين ينحاز إلى رأي: أكثر الدول سكاناً بالإضافة إلى تلك التي أحتاجها شخصياً. المستهلك الذي يمرّر ",[18,6174,6175],{},"default-country=\"678\""," (فانواتو) عند التركيب دون أيّ بيانات دولة مخبّأة لن يحصل على شيء من هذا البحث. بعد ثوانٍ قليلة، حين يعود نداء الشبكة، سيحسم المنتقي القيمة بشكل صحيح. غاية الاحتياطي أن تعمل الحالة الشائعة (95%) دون وميض، والذيل الطويل ينتهي إلى الصحيح.",[11,6178,6179],{},"داخل التطبيق تستطيع انتظار النداء. أمّا في المكتبة فلا تقدر على إلزام كلّ مستهلك بتعليق تركيب مكوّنه انتظاراً لواجهة برمجية طرف ثالث، ولا تستطيع في المقابل شحن 80 كيلوبايت من بيانات الدول بشكل فوري. الجدول المتزامن هو المخرج الثالث.",[26,6181,6183],{"id":6182},"آخر-الاختيارات-كفاصل-عند-التعادل","آخر الاختيارات كفاصل عند التعادل",[11,6185,6186,6187,6189],{},"مطابقة أطول بادئة في الطبقة الثالثة ليست دائماً واضحة بلا لبس. الرمز ",[18,6188,2090],{}," يطابق كتلة NANP كاملة، أكثر من 25 دولة. الأبجدي الأوّل يعني \"ساموا الأمريكية\" أو أيّاً كانت الدولة الأسبق أبجدياً في كلّ مرّة. هذا ليس افتراضاً مفيداً لأيّ أحد.",[11,6191,6192,6193,337],{},"لذلك تقرأ الطبقة الثالثة من ",[18,6194,1186],{},[40,6196,6198],{"className":1893,"code":6197,"language":1895,"meta":45,"style":45},"const recents = readRecents();\nconst recentHit = recents\n  .map((iso2) => group.find((c) => c.value === iso2))\n  .find((c): c is CountryOption => Boolean(c));\nif (recentHit) return { country: recentHit, nationalNumber };\nreturn { country: group[0], nationalNumber };\n",[18,6199,6200,6214,6226,6264,6295,6307],{"__ignoreMap":45},[49,6201,6202,6204,6207,6209,6212],{"class":51,"line":52},[49,6203,330],{"class":63},[49,6205,6206],{"class":333}," recents",[49,6208,343],{"class":63},[49,6210,6211],{"class":262}," readRecents",[49,6213,3477],{"class":55},[49,6215,6216,6218,6221,6223],{"class":51,"line":82},[49,6217,330],{"class":63},[49,6219,6220],{"class":333}," recentHit",[49,6222,343],{"class":63},[49,6224,6225],{"class":55}," recents\n",[49,6227,6228,6231,6234,6236,6238,6240,6242,6245,6247,6249,6252,6254,6256,6259,6261],{"class":51,"line":88},[49,6229,6230],{"class":55},"  .",[49,6232,6233],{"class":262},"map",[49,6235,801],{"class":55},[49,6237,5344],{"class":601},[49,6239,807],{"class":55},[49,6241,648],{"class":63},[49,6243,6244],{"class":55}," group.",[49,6246,6088],{"class":262},[49,6248,801],{"class":55},[49,6250,6251],{"class":601},"c",[49,6253,807],{"class":55},[49,6255,648],{"class":63},[49,6257,6258],{"class":55}," c.value ",[49,6260,654],{"class":63},[49,6262,6263],{"class":55}," iso2))\n",[49,6265,6266,6268,6270,6272,6274,6276,6278,6281,6284,6286,6289,6292],{"class":51,"line":94},[49,6267,6230],{"class":55},[49,6269,6088],{"class":262},[49,6271,801],{"class":55},[49,6273,6251],{"class":601},[49,6275,2433],{"class":55},[49,6277,337],{"class":63},[49,6279,6280],{"class":601}," c",[49,6282,6283],{"class":63}," is",[49,6285,4665],{"class":262},[49,6287,6288],{"class":63}," =>",[49,6290,6291],{"class":262}," Boolean",[49,6293,6294],{"class":55},"(c));\n",[49,6296,6297,6299,6302,6304],{"class":51,"line":363},[49,6298,3249],{"class":63},[49,6300,6301],{"class":55}," (recentHit) ",[49,6303,2197],{"class":63},[49,6305,6306],{"class":55}," { country: recentHit, nationalNumber };\n",[49,6308,6309,6311,6313,6315],{"class":51,"line":374},[49,6310,2197],{"class":63},[49,6312,2922],{"class":55},[49,6314,2839],{"class":333},[49,6316,2927],{"class":55},[11,6318,6319,6320,6323,6324,6326],{},"يكتب المنتقي في ",[18,6321,6322],{},"ali_ui_country_recents_v1"," كلّما اختار المستخدم دولةً صراحةً، ويقرأ المطابِق منها عند حصول الالتباس. إن كنتَ تعيش في كندا وسبق أن استعملتَ الحقل، فأوّل ",[18,6325,1977],{}," تكتبه سيُحلّ إلى كندا. وإن لم تستعمله من قبل، ستحصل على الاحتياطي الأبجدي، وهو خطأ نعم، لكنّه يقع مرّة واحدة فقط.",[11,6328,6329],{},"لمسة صغيرة، لكنّها من ذلك النوع الذي يحوّل \"هذا يعمل تقنياً\" إلى \"هذا مبنيّ بيدِ من استعمله فعلاً\".",[26,6331,6333],{"id":6332},"عن-ماذا-كانت-إعادة-كتابة-المكتبة-حقيقةً","عن ماذا كانت إعادة كتابة المكتبة حقيقةً",[11,6335,6336,6337,6340,6341,6344],{},"كانت النسخة الأصلية معنيّة بالصحّة: اضطراب موضع المؤشّر، وأسباب التحقّق، ومعالجة RTL\u002FLTR, والحالات السبع المحدّدة التي على حقل الهاتف أن يميّز بينها. لم يختفِ أيّ من ذلك. كلّه لا يزال حاضراً في هذه النسخة: الدالّة المسنِدة ",[18,6338,6339],{},"dropLeadingZeros",", والقيم السبع لـ enum المسمّى ",[18,6342,6343],{},"PhoneValidationReason",", ونمط التنسيق ثنائي الاتجاه القائم على غلاف LTR فوق مُدخل RTL. تلك الأشياء لم تكن خاطئة، بل كانت الأساس الذي يُبنى عليه.",[11,6346,6347],{},"ما أضافته إعادة كتابة المكتبة كلّه يتعلّق بحقيقة أنّها لم تعد تخدم تطبيقاً واحداً. فصل بين ISO2 ورمز الاتصال يصمد أمام رموز الاتصال الملتبسة. سلسلة اكتشاف بيئية قابلة للاستبدال وصالحة للعمل دون اتصال. منتقٍ يعمل popover على الحاسوب وdrawer على الهاتف، ولكلّ حالة صنفها الخاصّ. أربعة عشر slot كي لا يشتبك نظام تصميم المستهلك مع المكوّن. جدول رموز اتصال متزامن لأنّ مستهلك المكتبة لا يستطيع انتظار الشبكة. آخر الاختيارات فاصلاً عند التعادل لأنّ \"الأبجدي الأوّل\" هو مظهر الكسل بعينه.",[11,6349,6350],{},"تضاعف عدد الأسطر تقريباً. النسخة الأصلية كانت 160 سطراً للمكوّن مع 540 سطراً للـ composable, بينما النسخة الجديدة عدّة مئات من الأسطر موزّعة على مكوّنَين واثنين من الـ composables. تضاعف السطح البرمجي أكثر من مرّة. ومع ذلك يظلّ النداء بلا تكوين قابلاً للاختصار في ثلاثة أسطر:",[40,6352,6354],{"className":42,"code":6353,"language":44,"meta":45,"style":45},"\u003CATellInput v-model:phone=\"phone\" v-model:country=\"country\" show-validation \u002F>\n",[18,6355,6356],{"__ignoreMap":45},[49,6357,6358,6360,6362,6364,6366,6368,6370,6372,6374,6376,6378,6380,6382,6384,6386,6388,6390,6392],{"class":51,"line":52},[49,6359,56],{"class":55},[49,6361,1865],{"class":59},[49,6363,4286],{"class":262},[49,6365,337],{"class":55},[49,6367,4186],{"class":262},[49,6369,67],{"class":55},[49,6371,71],{"class":70},[49,6373,4186],{"class":55},[49,6375,71],{"class":70},[49,6377,4286],{"class":262},[49,6379,337],{"class":55},[49,6381,1959],{"class":262},[49,6383,67],{"class":55},[49,6385,71],{"class":70},[49,6387,1959],{"class":55},[49,6389,71],{"class":70},[49,6391,5233],{"class":262},[49,6393,885],{"class":55},[11,6395,6396],{},"لكلّ prop جديد افتراضي يعكس ما كانت النسخة الداخلية تفعله تلقائياً. من يريد \"حقل هاتف فحسب\" يحصل عليه. ومن يريد \"حقل هاتف بأعلامنا، وقائمة دولنا، ومسند بحثنا، ورسائل أخطائنا\" يجد كلّ نقاط الفصل التي يحتاجها.",[11,6398,6399,6400,6403],{},"الدرس الذي سأحمله معي إلى المرّة القادمة: جعل مكوّن قابلاً للنشر يتعلّق أساساً برصد الآراء التي فرضتَها على نفسك، وتحويل كلّ واحد منها إلى نقطة فصل. كلّ ",[18,6401,6402],{},"restcountries.com"," مثبّت، وكلّ احتياطي \"السعودية ومصر\"، وكلّ قائمة منسدلة ظاهرة بلا انقطاع: تلك الأمور لم تكن خطأً بالنسبة للتطبيق، لكنّها الأمور التي لن تنجو مع المستهلك التالي. ابحث عنها، وسمِّها، واستبدل كلاً منها بافتراضي مع إمكانية تجاوز. الافتراضي يُبقي مستهلكك القديم راضياً، والتجاوز يفتح الباب للجديد.",[219,6405,6406],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}",{"title":45,"searchDepth":82,"depth":82,"links":6408},[6409,6410,6411,6412,6413,6414,6415,6416],{"id":1886,"depth":82,"text":1887},{"id":2285,"depth":82,"text":2286},{"id":3317,"depth":82,"text":3318},{"id":4257,"depth":82,"text":4258},{"id":4411,"depth":82,"text":4412},{"id":5493,"depth":82,"text":5494},{"id":6182,"depth":82,"text":6183},{"id":6332,"depth":82,"text":6333},"2026-05-21","تحويل حقل هاتف من داخل تطبيق واحد إلى مكوّن Vue مُنشَر استوجب تغييرات جوهرية. تجربة استخدام تكتشف الدولة قبل أيّ تدخّل، وفصل بين رمز ISO2 ورقم الاتصال، ومنتقي متجاوب عبر Popover وDrawer، وSlots لكل شيء.",[1840,1847,6420,6421,6422,6423,4271,6424,6425,1846],"حقل هاتف","libphonenumber-js","اكتشاف الدولة","Popover متجاوب","defineModel","Slots",{},"\u002Fwrites-ar\u002Fatell-input-the-library-rewrite",{"title":1857,"description":6418},"writes-ar\u002Fatell-input-the-library-rewrite","8wJPO0VSknxZ5ZTju80YmwbagcwC1H2lmP5kn104P24",{"id":6432,"title":6433,"body":6434,"date":10518,"description":10519,"description_ar":1836,"draft":1837,"extension":1838,"image":1836,"keywords":10520,"lang":1849,"meta":10527,"navigation":324,"path":10528,"seo":10529,"stem":10530,"title_ar":1836,"updatedAt":10518,"__hash__":10531},"writesAr\u002Fwrites-ar\u002Fanatomy-of-adatatable.md","تشريح ADataTable، جدول بيانات مُعمَّم بأنواع في Vue",{"type":8,"value":6435,"toc":10495},[6436,6443,6450,6453,6460,6463,6775,6785,6788,7066,7069,7081,7099,7114,7121,7134,7152,7158,7304,7336,7342,7423,7448,7469,7475,7484,7501,7701,7704,7749,7752,7757,7857,7863,7885,7892,7918,7925,7934,8139,8144,8288,8302,8319,8323,8334,8337,8404,8407,8670,8673,8724,8742,8749,8760,8848,8851,8950,8953,8975,8982,8989,9060,9073,9081,9088,9208,9214,9230,9244,9257,9273,9276,9336,9339,9343,9346,9382,9389,9392,9514,9517,9520,9771,9778,9781,9792,9809,9911,9923,9933,9938,10054,10057,10064,10149,10156,10160,10163,10169,10220,10226,10229,10300,10314,10320,10393,10406,10410,10413,10480,10492],[11,6437,6438,6439,6442],{},"كتبتُ جدول البيانات نفسه ثلاث مرات. مرةً على هيئة كتلة JSX، ومرةً على شكل كائن إعدادات مبني على class اسمه ",[18,6440,6441],{},"IColumn","، والمرة الثالثة، وهي التي بقيت، مكوّن Vue مُعمَّم ومكتوب بأنواع، مع نوع عمود على هيئة سكيمة. في تلك المرة الثالثة تحديدًا، توقّف الجدول عن أن يكون الشيء الذي أخاف من الاقتراب منه.",[11,6444,6445,6446,6449],{},"هذه التدوينة جولة في شكله الحالي. أضعه في حزمة الـ UI الخاصة بي باسم ",[18,6447,6448],{},"ADataTable","، والمثير فيه ليس حيلة بعينها، بل أن المكوّن كفّ عن التضخم. كل طلب ميزة على مدار السنة الأخيرة استوعبته التجريدات الموجودة أصلًا، دون أن أضيف ولو prop جديد. هذه هي صيغة \"الاكتمال\" التي كنتُ أطاردها.",[11,6451,6452],{},"سأمرّ على الملفات بترتيب قريب من ترتيب تدفّق الطلب داخلها، متوقفًا عند ما يستحق الشرح.",[26,6454,6456,6457,6459],{"id":6455},"من-كلاس-icolumn-إلى-interface-مكتوب-بأنواع","من كلاس ",[18,6458,6441],{}," إلى interface مكتوب بأنواع",[11,6461,6462],{},"قبل عامين كان لديّ class بهذا الشكل:",[40,6464,6468],{"className":6465,"code":6466,"language":6467,"meta":45,"style":45},"language-javascript shiki shiki-themes github-light github-dark","export class IColumn {\n  constructor(options) {\n    const { title, rowKey, type = 'default', sortable = false, \u002F* ... *\u002F } = options;\n    this.title = title;\n    this.rowKey = rowKey;\n    this.sorter = sorter || ((a, b) => a[this.rowKey] > b[this.rowKey]);\n    this.headerComponent = this.processComponent(headerComponent);\n    this.rowComponent = this.processComponent(rowComponent);\n    \u002F\u002F ...\n  }\n\n  processComponent(component) {\n    if (!component) return undefined;\n    return {\n      is: component.is || '',\n      props: typeof component.props === 'function' ? component.props : () => component.props,\n      events: typeof component.events === 'function' ? component.events : () => component.events,\n    };\n  }\n}\n","javascript",[18,6469,6470,6481,6492,6536,6549,6561,6609,6629,6647,6652,6656,6660,6672,6689,6695,6706,6735,6762,6767,6771],{"__ignoreMap":45},[49,6471,6472,6474,6476,6479],{"class":51,"line":52},[49,6473,3341],{"class":63},[49,6475,999],{"class":63},[49,6477,6478],{"class":262}," IColumn",[49,6480,1625],{"class":55},[49,6482,6483,6486,6488,6490],{"class":51,"line":82},[49,6484,6485],{"class":63},"  constructor",[49,6487,625],{"class":55},[49,6489,5415],{"class":601},[49,6491,2149],{"class":55},[49,6493,6494,6496,6498,6501,6503,6506,6508,6510,6512,6515,6517,6520,6522,6524,6526,6529,6531,6533],{"class":51,"line":88},[49,6495,2204],{"class":63},[49,6497,701],{"class":55},[49,6499,6500],{"class":333},"title",[49,6502,159],{"class":55},[49,6504,6505],{"class":333},"rowKey",[49,6507,159],{"class":55},[49,6509,307],{"class":333},[49,6511,343],{"class":63},[49,6513,6514],{"class":70}," 'default'",[49,6516,159],{"class":55},[49,6518,6519],{"class":333},"sortable",[49,6521,343],{"class":63},[49,6523,3299],{"class":333},[49,6525,159],{"class":55},[49,6527,6528],{"class":2472},"\u002F* ... *\u002F",[49,6530,712],{"class":55},[49,6532,67],{"class":63},[49,6534,6535],{"class":55}," options;\n",[49,6537,6538,6541,6544,6546],{"class":51,"line":94},[49,6539,6540],{"class":333},"    this",[49,6542,6543],{"class":55},".title ",[49,6545,67],{"class":63},[49,6547,6548],{"class":55}," title;\n",[49,6550,6551,6553,6556,6558],{"class":51,"line":363},[49,6552,6540],{"class":333},[49,6554,6555],{"class":55},".rowKey ",[49,6557,67],{"class":63},[49,6559,6560],{"class":55}," rowKey;\n",[49,6562,6563,6565,6568,6570,6573,6575,6578,6581,6583,6586,6588,6590,6593,6596,6599,6601,6604,6606],{"class":51,"line":374},[49,6564,6540],{"class":333},[49,6566,6567],{"class":55},".sorter ",[49,6569,67],{"class":63},[49,6571,6572],{"class":55}," sorter ",[49,6574,2693],{"class":63},[49,6576,6577],{"class":55}," ((",[49,6579,6580],{"class":601},"a",[49,6582,159],{"class":55},[49,6584,6585],{"class":601},"b",[49,6587,807],{"class":55},[49,6589,648],{"class":63},[49,6591,6592],{"class":55}," a[",[49,6594,6595],{"class":333},"this",[49,6597,6598],{"class":55},".rowKey] ",[49,6600,5443],{"class":63},[49,6602,6603],{"class":55}," b[",[49,6605,6595],{"class":333},[49,6607,6608],{"class":55},".rowKey]);\n",[49,6610,6611,6613,6616,6618,6621,6623,6626],{"class":51,"line":385},[49,6612,6540],{"class":333},[49,6614,6615],{"class":55},".headerComponent ",[49,6617,67],{"class":63},[49,6619,6620],{"class":333}," this",[49,6622,527],{"class":55},[49,6624,6625],{"class":262},"processComponent",[49,6627,6628],{"class":55},"(headerComponent);\n",[49,6630,6631,6633,6636,6638,6640,6642,6644],{"class":51,"line":662},[49,6632,6540],{"class":333},[49,6634,6635],{"class":55},".rowComponent ",[49,6637,67],{"class":63},[49,6639,6620],{"class":333},[49,6641,527],{"class":55},[49,6643,6625],{"class":262},[49,6645,6646],{"class":55},"(rowComponent);\n",[49,6648,6649],{"class":51,"line":691},[49,6650,6651],{"class":2472},"    \u002F\u002F ...\n",[49,6653,6654],{"class":51,"line":696},[49,6655,2589],{"class":55},[49,6657,6658],{"class":51,"line":723},[49,6659,325],{"emptyLinePlaceholder":324},[49,6661,6662,6665,6667,6670],{"class":51,"line":744},[49,6663,6664],{"class":262},"  processComponent",[49,6666,625],{"class":55},[49,6668,6669],{"class":601},"component",[49,6671,2149],{"class":55},[49,6673,6674,6676,6678,6680,6683,6685,6687],{"class":51,"line":768},[49,6675,2138],{"class":63},[49,6677,2183],{"class":55},[49,6679,2454],{"class":63},[49,6681,6682],{"class":55},"component) ",[49,6684,2197],{"class":63},[49,6686,5440],{"class":333},[49,6688,319],{"class":55},[49,6690,6691,6693],{"class":51,"line":779},[49,6692,2945],{"class":63},[49,6694,1625],{"class":55},[49,6696,6697,6700,6702,6704],{"class":51,"line":784},[49,6698,6699],{"class":55},"      is: component.is ",[49,6701,2693],{"class":63},[49,6703,2162],{"class":70},[49,6705,360],{"class":55},[49,6707,6708,6711,6714,6717,6719,6721,6724,6726,6728,6730,6732],{"class":51,"line":789},[49,6709,6710],{"class":55},"      props: ",[49,6712,6713],{"class":63},"typeof",[49,6715,6716],{"class":55}," component.props ",[49,6718,654],{"class":63},[49,6720,3818],{"class":70},[49,6722,6723],{"class":63}," ?",[49,6725,6716],{"class":55},[49,6727,337],{"class":63},[49,6729,4441],{"class":55},[49,6731,648],{"class":63},[49,6733,6734],{"class":55}," component.props,\n",[49,6736,6737,6740,6742,6745,6747,6749,6751,6753,6755,6757,6759],{"class":51,"line":820},[49,6738,6739],{"class":55},"      events: ",[49,6741,6713],{"class":63},[49,6743,6744],{"class":55}," component.events ",[49,6746,654],{"class":63},[49,6748,3818],{"class":70},[49,6750,6723],{"class":63},[49,6752,6744],{"class":55},[49,6754,337],{"class":63},[49,6756,4441],{"class":55},[49,6758,648],{"class":63},[49,6760,6761],{"class":55}," component.events,\n",[49,6763,6764],{"class":51,"line":829},[49,6765,6766],{"class":55},"    };\n",[49,6768,6769],{"class":51,"line":834},[49,6770,2589],{"class":55},[49,6772,6773],{"class":51,"line":844},[49,6774,1719],{"class":55},[11,6776,6777,6778,6781,6782,527],{},"مهمّة هذا الكلاس كانت توحيد شكل الـ props والـ events. لو مررتَ كائنًا ثابتًا، فإنه يُلفّه داخل ",[18,6779,6780],{},"() => object"," كي يستطيع الجدول استدعاءه دائمًا كدالة. الفكرة أنيقة على الورق، ومقبولة تمامًا في JavaScript وحده. المشكلة بدأت حين دخل TypeScript إلى الصورة: أنواع الصفوف المُعمَّمة لا تتدفق بنظافة عبر class يعمل وقت التشغيل، والمستهلك يفقد استنتاج الأنواع بالضبط عند اللحظة التي يريد فيها الوصول إلى ",[18,6783,6784],{},"row.user.name",[11,6786,6787],{},"الشكل الحالي مجرد interface بسيط ومكتوب بأنواع:",[40,6789,6791],{"className":1893,"code":6790,"language":1895,"meta":45,"style":45},"export interface IDataTableColumn\u003CT = unknown> {\n  title: string | IDataTableRowFunction\u003CT>;\n  key: string | IDataTableRowFunction\u003CT>;\n  props?: Record\u003Cstring, unknown> | IDataTableRowFunction\u003CT>;\n  type?: 'default' | 'actions' | 'selection';\n  disabled?: boolean | IDataTableRowFunction\u003CT>;\n\n  headRender?: () => Array\u003CIDataTableVNodeChild> | IDataTableVNodeChild;\n  bodyRender?: IDataTableRenderFunction\u003CT>;\n\n  sortable?: boolean;\n  sorter?: (row1: T, row2: T) => number;\n  sortValue?: ISortValue;\n  extras?: Record\u003Cstring, unknown> | IDataTableRowFunction\u003CT>;\n}\n",[18,6792,6793,6814,6835,6854,6884,6905,6924,6928,6956,6972,6976,6987,7021,7033,7062],{"__ignoreMap":45},[49,6794,6795,6797,6800,6803,6805,6808,6810,6812],{"class":51,"line":52},[49,6796,3341],{"class":63},[49,6798,6799],{"class":63}," interface",[49,6801,6802],{"class":262}," IDataTableColumn",[49,6804,56],{"class":55},[49,6806,6807],{"class":262},"T",[49,6809,343],{"class":63},[49,6811,4446],{"class":333},[49,6813,3377],{"class":55},[49,6815,6816,6819,6821,6823,6825,6828,6830,6832],{"class":51,"line":82},[49,6817,6818],{"class":601},"  title",[49,6820,337],{"class":63},[49,6822,607],{"class":333},[49,6824,679],{"class":63},[49,6826,6827],{"class":262}," IDataTableRowFunction",[49,6829,56],{"class":55},[49,6831,6807],{"class":262},[49,6833,6834],{"class":55},">;\n",[49,6836,6837,6840,6842,6844,6846,6848,6850,6852],{"class":51,"line":88},[49,6838,6839],{"class":601},"  key",[49,6841,337],{"class":63},[49,6843,607],{"class":333},[49,6845,679],{"class":63},[49,6847,6827],{"class":262},[49,6849,56],{"class":55},[49,6851,6807],{"class":262},[49,6853,6834],{"class":55},[49,6855,6856,6859,6861,6863,6865,6867,6869,6872,6874,6876,6878,6880,6882],{"class":51,"line":94},[49,6857,6858],{"class":601},"  props",[49,6860,3946],{"class":63},[49,6862,5455],{"class":262},[49,6864,56],{"class":55},[49,6866,1914],{"class":333},[49,6868,159],{"class":55},[49,6870,6871],{"class":333},"unknown",[49,6873,5553],{"class":55},[49,6875,5433],{"class":63},[49,6877,6827],{"class":262},[49,6879,56],{"class":55},[49,6881,6807],{"class":262},[49,6883,6834],{"class":55},[49,6885,6886,6889,6891,6893,6895,6898,6900,6903],{"class":51,"line":363},[49,6887,6888],{"class":601},"  type",[49,6890,3946],{"class":63},[49,6892,6514],{"class":70},[49,6894,679],{"class":63},[49,6896,6897],{"class":70}," 'actions'",[49,6899,679],{"class":63},[49,6901,6902],{"class":70}," 'selection'",[49,6904,319],{"class":55},[49,6906,6907,6910,6912,6914,6916,6918,6920,6922],{"class":51,"line":374},[49,6908,6909],{"class":601},"  disabled",[49,6911,3946],{"class":63},[49,6913,4678],{"class":333},[49,6915,679],{"class":63},[49,6917,6827],{"class":262},[49,6919,56],{"class":55},[49,6921,6807],{"class":262},[49,6923,6834],{"class":55},[49,6925,6926],{"class":51,"line":385},[49,6927,325],{"emptyLinePlaceholder":324},[49,6929,6930,6933,6935,6937,6939,6942,6944,6947,6949,6951,6954],{"class":51,"line":662},[49,6931,6932],{"class":262},"  headRender",[49,6934,3946],{"class":63},[49,6936,4441],{"class":55},[49,6938,648],{"class":63},[49,6940,6941],{"class":262}," Array",[49,6943,56],{"class":55},[49,6945,6946],{"class":262},"IDataTableVNodeChild",[49,6948,5553],{"class":55},[49,6950,5433],{"class":63},[49,6952,6953],{"class":262}," IDataTableVNodeChild",[49,6955,319],{"class":55},[49,6957,6958,6961,6963,6966,6968,6970],{"class":51,"line":691},[49,6959,6960],{"class":601},"  bodyRender",[49,6962,3946],{"class":63},[49,6964,6965],{"class":262}," IDataTableRenderFunction",[49,6967,56],{"class":55},[49,6969,6807],{"class":262},[49,6971,6834],{"class":55},[49,6973,6974],{"class":51,"line":696},[49,6975,325],{"emptyLinePlaceholder":324},[49,6977,6978,6981,6983,6985],{"class":51,"line":723},[49,6979,6980],{"class":601},"  sortable",[49,6982,3946],{"class":63},[49,6984,4678],{"class":333},[49,6986,319],{"class":55},[49,6988,6989,6992,6994,6996,6999,7001,7004,7006,7009,7011,7013,7015,7017,7019],{"class":51,"line":744},[49,6990,6991],{"class":262},"  sorter",[49,6993,3946],{"class":63},[49,6995,2183],{"class":55},[49,6997,6998],{"class":601},"row1",[49,7000,337],{"class":63},[49,7002,7003],{"class":262}," T",[49,7005,159],{"class":55},[49,7007,7008],{"class":601},"row2",[49,7010,337],{"class":63},[49,7012,7003],{"class":262},[49,7014,807],{"class":55},[49,7016,648],{"class":63},[49,7018,3775],{"class":333},[49,7020,319],{"class":55},[49,7022,7023,7026,7028,7031],{"class":51,"line":768},[49,7024,7025],{"class":601},"  sortValue",[49,7027,3946],{"class":63},[49,7029,7030],{"class":262}," ISortValue",[49,7032,319],{"class":55},[49,7034,7035,7038,7040,7042,7044,7046,7048,7050,7052,7054,7056,7058,7060],{"class":51,"line":779},[49,7036,7037],{"class":601},"  extras",[49,7039,3946],{"class":63},[49,7041,5455],{"class":262},[49,7043,56],{"class":55},[49,7045,1914],{"class":333},[49,7047,159],{"class":55},[49,7049,6871],{"class":333},[49,7051,5553],{"class":55},[49,7053,5433],{"class":63},[49,7055,6827],{"class":262},[49,7057,56],{"class":55},[49,7059,6807],{"class":262},[49,7061,6834],{"class":55},[49,7063,7064],{"class":51,"line":784},[49,7065,1719],{"class":55},[11,7067,7068],{},"تغيّرت ثلاثة أشياء، وكل واحد منها ترك أثرًا واضحًا.",[11,7070,7071,7072,7075,7076,7080],{},"أولًا، لا class. العمود صار كائنًا عاديًا. TypeScript يُضيّق نوعه، والمحرر يقدّم autocomplete جاهزًا، ويتسلسل نظيفًا لو أردتُ يومًا أن أُغذّي الجدول من JSON. أما مهمة التوحيد التي كان يتولّاها الكلاس وقت الإنشاء، فقد نقلتُها إلى دالة مساعدة صغيرة في مسار الـ render اسمها ",[18,7073,7074],{},"handleProps","، وسنمرّ عليها بعد قليل. الكلفة صارت فرعًا واحدًا لكل render لخلية بدل فرع واحد لكل ",[7077,7078,7079],"em",{},"تعريف"," عمود، والمكسب أن العمود يظل قيمة لا كائنًا حيًّا وقت التشغيل.",[11,7082,7083,7084,7086,7087,7090,7091,7094,7095,7098],{},"ثانيًا، صار العمود مُعمَّمًا على ",[18,7085,6807],{},". حين تكتب ",[18,7088,7089],{},"IDataTableColumn\u003CUser>","، فإن ",[18,7092,7093],{},"key"," يتحوّل إلى ",[18,7096,7097],{},"string | (row: User) => ...","، وهذا النوع يتدفّق إلى كل callback لاحق. في اليوم الذي أضفتُ فيه هذا التعميم، كشف ثلاثة مواضع استدعاء في العمل أخطاء في أنواع أعمدة كانت مكسورة بصمت.",[11,7100,7101,7102,7105,7106,7109,7110,7113],{},"ثالثًا، انقسمت مسؤوليات الـ render انقسامًا نظيفًا. ",[18,7103,7104],{},"headRender"," لخلية الرأس فقط، و",[18,7107,7108],{},"bodyRender"," لخلية الجسم. نسخة الكلاس كانت تجمع الاثنين في ",[18,7111,7112],{},"rowComponent"," واحد يتفرّع داخليًا، مما يعني أن حالة \"رأس مخصّص وجسم افتراضي\" كانت تتطلب توليفة غير موثّقة من الحقول الفارغة.",[11,7115,7116,7117,7120],{},"لا شيء من هذا فكرة جديدة. الانتقال من class يعمل وقت التشغيل إلى نوع على هيئة سكيمة هو قصة معظم مشاريع الـ UI في السنوات الخمس الأخيرة. أذكره فقط لأنني كتبتُ نسخة الكلاس ",[7077,7118,7119],{},"أولًا","، وواصلتُ إطلاقها لفترة طويلة بعد أن أصبحت نسخة الأنواع أرخص بكثير. تكلفة الانتقال لم تكن في إعادة الكتابة نفسها، بل في الاعتراف بأن التصميم الأول قد استُنفد.",[26,7122,7124,7125,394,7128,394,7131],{"id":7123},"ثلاثي-الـ-render-athead-وatbody-وatcell","ثلاثي الـ render: ",[18,7126,7127],{},"ATHead",[18,7129,7130],{},"ATBody",[18,7132,7133],{},"ATCell",[11,7135,7136,7137,394,7140,7143,7144,7147,7148,7151],{},"مكوّن الجدول لا يعرض الصفوف بشكل مباشر. هو يعرض ",[18,7138,7139],{},"\u003Cthead>",[18,7141,7142],{},"\u003Ctbody>","، وهذان بدورهما يعرضان عناصر ",[18,7145,7146],{},"\u003Ctr>"," التي تُركّب مكوّنات ",[18,7149,7150],{},"\u003CATCell>",". الفصل محكم بحيث يبقى لكل ملف عمل واحد فقط.",[11,7153,7154,7157],{},[18,7155,7156],{},"ATHead.vue"," هو الأبسط:",[40,7159,7161],{"className":42,"code":7160,"language":44,"meta":45,"style":45},"\u003Ctemplate>\n  \u003Cthead>\n    \u003Ctr>\n      \u003CATCell\n        v-for=\"(column, collIndex) in props.columns\"\n        :key=\"collIndex\"\n        :binder=\"column.props\"\n        :cell=\"column\"\n        :head=\"true\"\n        :record=\"{} as S\"\n        :row-index=\"collIndex\"\n        @update-sorter=\"emit('updateSorter', collIndex)\"\n      \u002F>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n\u003C\u002Ftemplate>\n",[18,7162,7163,7171,7179,7187,7195,7205,7215,7225,7235,7245,7255,7264,7274,7279,7288,7296],{"__ignoreMap":45},[49,7164,7165,7167,7169],{"class":51,"line":52},[49,7166,56],{"class":55},[49,7168,839],{"class":59},[49,7170,79],{"class":55},[49,7172,7173,7175,7177],{"class":51,"line":82},[49,7174,847],{"class":55},[49,7176,1519],{"class":59},[49,7178,79],{"class":55},[49,7180,7181,7183,7185],{"class":51,"line":88},[49,7182,864],{"class":55},[49,7184,1522],{"class":59},[49,7186,79],{"class":55},[49,7188,7189,7192],{"class":51,"line":94},[49,7190,7191],{"class":55},"      \u003C",[49,7193,7194],{"class":59},"ATCell\n",[49,7196,7197,7200,7202],{"class":51,"line":363},[49,7198,7199],{"class":262},"        v-for",[49,7201,67],{"class":55},[49,7203,7204],{"class":70},"\"(column, collIndex) in props.columns\"\n",[49,7206,7207,7210,7212],{"class":51,"line":374},[49,7208,7209],{"class":262},"        :key",[49,7211,67],{"class":55},[49,7213,7214],{"class":70},"\"collIndex\"\n",[49,7216,7217,7220,7222],{"class":51,"line":385},[49,7218,7219],{"class":262},"        :binder",[49,7221,67],{"class":55},[49,7223,7224],{"class":70},"\"column.props\"\n",[49,7226,7227,7230,7232],{"class":51,"line":662},[49,7228,7229],{"class":262},"        :cell",[49,7231,67],{"class":55},[49,7233,7234],{"class":70},"\"column\"\n",[49,7236,7237,7240,7242],{"class":51,"line":691},[49,7238,7239],{"class":262},"        :head",[49,7241,67],{"class":55},[49,7243,7244],{"class":70},"\"true\"\n",[49,7246,7247,7250,7252],{"class":51,"line":696},[49,7248,7249],{"class":262},"        :record",[49,7251,67],{"class":55},[49,7253,7254],{"class":70},"\"{} as S\"\n",[49,7256,7257,7260,7262],{"class":51,"line":723},[49,7258,7259],{"class":262},"        :row-index",[49,7261,67],{"class":55},[49,7263,7214],{"class":70},[49,7265,7266,7269,7271],{"class":51,"line":744},[49,7267,7268],{"class":262},"        @update-sorter",[49,7270,67],{"class":55},[49,7272,7273],{"class":70},"\"emit('updateSorter', collIndex)\"\n",[49,7275,7276],{"class":51,"line":768},[49,7277,7278],{"class":55},"      \u002F>\n",[49,7280,7281,7284,7286],{"class":51,"line":779},[49,7282,7283],{"class":55},"    \u003C\u002F",[49,7285,1522],{"class":59},[49,7287,79],{"class":55},[49,7289,7290,7292,7294],{"class":51,"line":784},[49,7291,930],{"class":55},[49,7293,1519],{"class":59},[49,7295,79],{"class":55},[49,7297,7298,7300,7302],{"class":51,"line":789},[49,7299,97],{"class":55},[49,7301,839],{"class":59},[49,7303,79],{"class":55},[11,7305,7306,7307,7309,7310,7312,7313,7316,7317,7320,7321,7324,7325,7327,7328,7331,7332,7335],{},"عنصر ",[18,7308,7146],{}," واحد، ومكوّن ",[18,7311,7150],{}," واحد لكل عمود مع ",[18,7314,7315],{},"head={true}",". أما الـ ",[18,7318,7319],{},"record"," فهو كائن فارغ اصطناعي مُحوَّل بالنوع إلى ",[18,7322,7323],{},"S",". الرأس لا يمتلك صفًا حقيقيًا، لكن ",[18,7326,7133],{}," مُعمَّم على السجل، فلا بدّ من شيء يشغل هذا الموضع. تحويل ",[18,7329,7330],{},"{} as S"," هو أقل الخيارات سوءًا. لو مرّرنا ",[18,7333,7334],{},"undefined"," فعلية، لأجبرنا كل خلية على فحص العدم عند كل render.",[11,7337,7338,7341],{},[18,7339,7340],{},"ATBody.vue"," يأخذ الشكل نفسه لكن بمستوى واحد أعلى:",[40,7343,7345],{"className":42,"code":7344,"language":44,"meta":45,"style":45},"\u003Ctbody>\n  \u003Ctr v-for=\"(dataItem, $index) in props.items\" :key=\"$index + '_dataItem'\">\n    \u003CATCell\n      v-for=\"(column, collIndex) in props.columns\"\n      :key=\"collIndex + '_dataItem_' + $index\"\n      :record=\"dataItem\"\n      :head=\"false\"\n      :row-index=\"$index\"\n      :cell=\"column\"\n      :binder=\"column.props\"\n    \u002F>\n  \u003C\u002Ftr>\n  \u003Cslot \u002F>\n\u003C\u002Ftbody>\n",[18,7346,7347,7355,7360,7365,7370,7375,7380,7385,7390,7395,7400,7405,7410,7415],{"__ignoreMap":45},[49,7348,7349,7351,7353],{"class":51,"line":52},[49,7350,56],{"class":55},[49,7352,1532],{"class":59},[49,7354,79],{"class":55},[49,7356,7357],{"class":51,"line":82},[49,7358,7359],{"class":55},"  \u003Ctr v-for=\"(dataItem, $index) in props.items\" :key=\"$index + '_dataItem'\">\n",[49,7361,7362],{"class":51,"line":88},[49,7363,7364],{"class":55},"    \u003CATCell\n",[49,7366,7367],{"class":51,"line":94},[49,7368,7369],{"class":55},"      v-for=\"(column, collIndex) in props.columns\"\n",[49,7371,7372],{"class":51,"line":363},[49,7373,7374],{"class":55},"      :key=\"collIndex + '_dataItem_' + $index\"\n",[49,7376,7377],{"class":51,"line":374},[49,7378,7379],{"class":55},"      :record=\"dataItem\"\n",[49,7381,7382],{"class":51,"line":385},[49,7383,7384],{"class":55},"      :head=\"false\"\n",[49,7386,7387],{"class":51,"line":662},[49,7388,7389],{"class":55},"      :row-index=\"$index\"\n",[49,7391,7392],{"class":51,"line":691},[49,7393,7394],{"class":55},"      :cell=\"column\"\n",[49,7396,7397],{"class":51,"line":696},[49,7398,7399],{"class":55},"      :binder=\"column.props\"\n",[49,7401,7402],{"class":51,"line":723},[49,7403,7404],{"class":55},"    \u002F>\n",[49,7406,7407],{"class":51,"line":744},[49,7408,7409],{"class":55},"  \u003C\u002Ftr>\n",[49,7411,7412],{"class":51,"line":768},[49,7413,7414],{"class":55},"  \u003Cslot \u002F>\n",[49,7416,7417,7419,7421],{"class":51,"line":779},[49,7418,97],{"class":55},[49,7420,1532],{"class":59},[49,7422,79],{"class":55},[11,7424,7425,7426,394,7429,7432,7433,7436,7437,7440,7441,7444,7445,7447],{},"المفاتيح المركّبة (",[18,7427,7428],{},"$index + '_dataItem'",[18,7430,7431],{},"collIndex + '_dataItem_' + $index",") ليست وسواسًا. مُطابِق Vue يهتم فقط بأن يكون المفتاح فريدًا ",[7077,7434,7435],{},"داخل قائمة الأب","، ولذلك ",[18,7438,7439],{},"$index"," وحده كافٍ. لكن حين أتصفح Vue DevTools وأحاول أن أعرف أيّ صف هو أيّ، فإن ظهور مفتاح بصيغة ",[18,7442,7443],{},"3_dataItem"," بدل مجرد ",[18,7446,2797],{}," يوفّر عليّ ثانية. هذا هو السبب الوحيد للاحقة.",[11,7449,7450,7451,7454,7455,7457,7458,7461,7462,7465,7466,7468],{},"الـ ",[18,7452,7453],{},"\u003Cslot \u002F>"," داخل ",[18,7456,7142],{}," هو الموضع الذي يحلّ فيه صفّ \"لا توجد بيانات\". المكوّن الخارجي يمرّر عبره ",[18,7459,7460],{},"\u003Ctr>\u003Ctd>...\u003C\u002Ftd>\u003C\u002Ftr>",". إبقاء حالة الفراغ صفًّا يمرّ عبر slot، بدل جعله عنصرًا منفصلًا، يعني أن حسابات ",[18,7463,7464],{},"colspan"," تعمل ضمن شبكة الـ ",[18,7467,7146],{}," نفسها، وأنماط انطباق الحدود لا تنكسر.",[11,7470,7471,7472,527],{},"العمل الحقيقي كلّه يجري داخل ",[18,7473,7474],{},"ATCell.vue",[26,7476,7478,7480,7481],{"id":7477},"atcell-مكوّن-واحد-بشكلَين-مبنيّ-عبر-h",[18,7479,7133],{},": مكوّن واحد بشكلَين، مبنيّ عبر ",[18,7482,7483],{},"h()",[11,7485,7486,7488,7489,7492,7493,7496,7497,7500],{},[18,7487,7133],{}," يعرض إما ",[18,7490,7491],{},"\u003Cth>"," وإما ",[18,7494,7495],{},"\u003Ctd>"," بحسب قيمة ",[18,7498,7499],{},"head",". بدل كتابة قالبَين، يبني الـ vnode ديناميكيًا:",[40,7502,7504],{"className":1893,"code":7503,"language":1895,"meta":45,"style":45},"const Cell = computed(() => {\n  return h(\n    props.head ? 'th' : 'td',\n    {\n      class: 'p-[16px] text-md gap-2 relative',\n      colspan: '1',\n      rowspan: '1',\n      ...handleProps\u003CT>(props.cell.props, props.record),\n      ...handleProps\u003CT>(props.binder, props.record),\n    },\n    h('div', { class: 'flex items-center gap-2 w-full' },\n      props.head\n        ? props.cell.sortable\n          ? [children(props.cell, props.record), sortButton(props.cell.sortValue?.applied)]\n          : children(props.cell, props.record)\n        : children(props.cell, props.record)\n    )\n  );\n});\n",[18,7505,7506,7523,7532,7549,7554,7564,7574,7583,7597,7610,7615,7634,7639,7647,7667,7678,7687,7692,7697],{"__ignoreMap":45},[49,7507,7508,7510,7513,7515,7517,7519,7521],{"class":51,"line":52},[49,7509,330],{"class":63},[49,7511,7512],{"class":333}," Cell",[49,7514,343],{"class":63},[49,7516,642],{"class":262},[49,7518,645],{"class":55},[49,7520,648],{"class":63},[49,7522,1625],{"class":55},[49,7524,7525,7527,7530],{"class":51,"line":82},[49,7526,2962],{"class":63},[49,7528,7529],{"class":262}," h",[49,7531,2114],{"class":55},[49,7533,7534,7537,7539,7542,7544,7547],{"class":51,"line":88},[49,7535,7536],{"class":55},"    props.head ",[49,7538,758],{"class":63},[49,7540,7541],{"class":70}," 'th'",[49,7543,259],{"class":63},[49,7545,7546],{"class":70}," 'td'",[49,7548,360],{"class":55},[49,7550,7551],{"class":51,"line":94},[49,7552,7553],{"class":55},"    {\n",[49,7555,7556,7559,7562],{"class":51,"line":363},[49,7557,7558],{"class":55},"      class: ",[49,7560,7561],{"class":70},"'p-[16px] text-md gap-2 relative'",[49,7563,360],{"class":55},[49,7565,7566,7569,7572],{"class":51,"line":374},[49,7567,7568],{"class":55},"      colspan: ",[49,7570,7571],{"class":70},"'1'",[49,7573,360],{"class":55},[49,7575,7576,7579,7581],{"class":51,"line":385},[49,7577,7578],{"class":55},"      rowspan: ",[49,7580,7571],{"class":70},[49,7582,360],{"class":55},[49,7584,7585,7588,7590,7592,7594],{"class":51,"line":662},[49,7586,7587],{"class":63},"      ...",[49,7589,7074],{"class":262},[49,7591,56],{"class":55},[49,7593,6807],{"class":262},[49,7595,7596],{"class":55},">(props.cell.props, props.record),\n",[49,7598,7599,7601,7603,7605,7607],{"class":51,"line":691},[49,7600,7587],{"class":63},[49,7602,7074],{"class":262},[49,7604,56],{"class":55},[49,7606,6807],{"class":262},[49,7608,7609],{"class":55},">(props.binder, props.record),\n",[49,7611,7612],{"class":51,"line":696},[49,7613,7614],{"class":55},"    },\n",[49,7616,7617,7620,7622,7625,7628,7631],{"class":51,"line":723},[49,7618,7619],{"class":262},"    h",[49,7621,625],{"class":55},[49,7623,7624],{"class":70},"'div'",[49,7626,7627],{"class":55},", { class: ",[49,7629,7630],{"class":70},"'flex items-center gap-2 w-full'",[49,7632,7633],{"class":55}," },\n",[49,7635,7636],{"class":51,"line":744},[49,7637,7638],{"class":55},"      props.head\n",[49,7640,7641,7644],{"class":51,"line":768},[49,7642,7643],{"class":63},"        ?",[49,7645,7646],{"class":55}," props.cell.sortable\n",[49,7648,7649,7652,7655,7658,7661,7664],{"class":51,"line":779},[49,7650,7651],{"class":63},"          ?",[49,7653,7654],{"class":55}," [",[49,7656,7657],{"class":262},"children",[49,7659,7660],{"class":55},"(props.cell, props.record), ",[49,7662,7663],{"class":262},"sortButton",[49,7665,7666],{"class":55},"(props.cell.sortValue?.applied)]\n",[49,7668,7669,7672,7675],{"class":51,"line":784},[49,7670,7671],{"class":63},"          :",[49,7673,7674],{"class":262}," children",[49,7676,7677],{"class":55},"(props.cell, props.record)\n",[49,7679,7680,7683,7685],{"class":51,"line":789},[49,7681,7682],{"class":63},"        :",[49,7684,7674],{"class":262},[49,7686,7677],{"class":55},[49,7688,7689],{"class":51,"line":820},[49,7690,7691],{"class":55},"    )\n",[49,7693,7694],{"class":51,"line":829},[49,7695,7696],{"class":55},"  );\n",[49,7698,7699],{"class":51,"line":834},[49,7700,388],{"class":55},[11,7702,7703],{},"ثم يكتفي القالب بتركيب الـ vnode المحسوب:",[40,7705,7707],{"className":42,"code":7706,"language":44,"meta":45,"style":45},"\u003Ctemplate>\n  \u003Ccomponent :is=\"Cell\" :key=\"props.cell.sortValue?.applied || 'ATCell'\" \u002F>\n\u003C\u002Ftemplate>\n",[18,7708,7709,7717,7741],{"__ignoreMap":45},[49,7710,7711,7713,7715],{"class":51,"line":52},[49,7712,56],{"class":55},[49,7714,839],{"class":59},[49,7716,79],{"class":55},[49,7718,7719,7721,7723,7726,7728,7731,7734,7736,7739],{"class":51,"line":82},[49,7720,847],{"class":55},[49,7722,6669],{"class":59},[49,7724,7725],{"class":262}," :is",[49,7727,67],{"class":55},[49,7729,7730],{"class":70},"\"Cell\"",[49,7732,7733],{"class":262}," :key",[49,7735,67],{"class":55},[49,7737,7738],{"class":70},"\"props.cell.sortValue?.applied || 'ATCell'\"",[49,7740,885],{"class":55},[49,7742,7743,7745,7747],{"class":51,"line":88},[49,7744,97],{"class":55},[49,7746,839],{"class":59},[49,7748,79],{"class":55},[11,7750,7751],{},"هنا شيئان يعملان بهدوء.",[11,7753,7754,7756],{},[18,7755,7074],{}," يقبل إما كائنًا عاديًا وإما دالة تأخذ الصف، ويُعيد كائنًا دائمًا:",[40,7758,7760],{"className":1893,"code":7759,"language":1895,"meta":45,"style":45},"const handleProps = \u003CT,>(val: IDataTableColumn\u003CT>['props'], record: T) => {\n  if (!val) return {};\n  return typeof val === 'function' ? val(record) : val;\n};\n",[18,7761,7762,7811,7827,7853],{"__ignoreMap":45},[49,7763,7764,7766,7769,7771,7774,7776,7779,7782,7784,7786,7788,7790,7793,7796,7799,7801,7803,7805,7807,7809],{"class":51,"line":52},[49,7765,330],{"class":63},[49,7767,7768],{"class":262}," handleProps",[49,7770,343],{"class":63},[49,7772,7773],{"class":55}," \u003C",[49,7775,6807],{"class":262},[49,7777,7778],{"class":55},",>(",[49,7780,7781],{"class":601},"val",[49,7783,337],{"class":63},[49,7785,6802],{"class":262},[49,7787,56],{"class":55},[49,7789,6807],{"class":262},[49,7791,7792],{"class":55},">[",[49,7794,7795],{"class":70},"'props'",[49,7797,7798],{"class":55},"], ",[49,7800,7319],{"class":601},[49,7802,337],{"class":63},[49,7804,7003],{"class":262},[49,7806,807],{"class":55},[49,7808,648],{"class":63},[49,7810,1625],{"class":55},[49,7812,7813,7815,7817,7819,7822,7824],{"class":51,"line":82},[49,7814,2449],{"class":63},[49,7816,2183],{"class":55},[49,7818,2454],{"class":63},[49,7820,7821],{"class":55},"val) ",[49,7823,2197],{"class":63},[49,7825,7826],{"class":55}," {};\n",[49,7828,7829,7831,7833,7836,7838,7840,7842,7845,7848,7850],{"class":51,"line":88},[49,7830,2962],{"class":63},[49,7832,3809],{"class":63},[49,7834,7835],{"class":55}," val ",[49,7837,654],{"class":63},[49,7839,3818],{"class":70},[49,7841,6723],{"class":63},[49,7843,7844],{"class":262}," val",[49,7846,7847],{"class":55},"(record) ",[49,7849,337],{"class":63},[49,7851,7852],{"class":55}," val;\n",[49,7854,7855],{"class":51,"line":94},[49,7856,5759],{"class":55},[11,7858,7859,7860,7862],{},"هذا هو نفس عقد \"قابل للاستدعاء دائمًا\" الذي كان كلاس ",[18,7861,6441],{}," القديم يفرضه وقت الإنشاء، لكنه انتقل الآن إلى مسار الـ render، حيث يكون أرخص في الحالة الشائعة (لا تُغلَّف الـ props الثابتة بلا داعٍ)، ويترك للمستهلك حرية الاختيار.",[11,7864,7865,7866,7869,7870,7873,7874,7877,7878,7881,7882,7884],{},"ترتيب الـ spread مقصود. ",[18,7867,7868],{},"props.cell.props"," يُنشر أولًا، ثم ",[18,7871,7872],{},"props.binder"," بعده. في الواقع، ",[18,7875,7876],{},"binder"," هو نفسه ",[18,7879,7880],{},"cell.props","، ولا يُمرَّر عبر prop منفصل إلا لأسباب تاريخية سأدمجها لو أعدتُ الكتابة. لكن حاليًا يفوز ",[18,7883,7876],{}," عند التعارض، وهذا هو ما يتوقعه المستدعون.",[1013,7886,7450,7888,7891],{"id":7887},"الـ-keysortvalueapplied-الذي-يبدو-خطأً-مطبعيًا",[18,7889,7890],{},":key=\"sortValue?.applied\""," الذي يبدو خطأً مطبعيًا",[11,7893,7894,7895,7898,7899,7902,7903,7906,7907,7909,7910,7913,7914,7917],{},"الشيء الثاني هو المفتاح الغريب ",[18,7896,7897],{},":key=\"props.cell.sortValue?.applied || 'ATCell'\"",". الـ ",[18,7900,7901],{},"\u003Ccomponent :is>"," يتفاعل أصلًا مع تغيّر الـ vnode، لكن الـ ",[18,7904,7905],{},":key"," هنا موجود لإجبار إعادة تركيب كاملة حين تنقلب حالة الفرز. وهذا بالضبط دوره. آلية التحديث الجزئي في Vue ذكية بما يكفي لعمل diff بين خرجَي ",[18,7908,7483],{},"، ولكن حين تأتي props الخلية من دالة على السجل ",[7077,7911,7912],{},"ويتغيّر"," السجل في اللحظة نفسها التي ينقلب فيها الفرز، فقد يتشبّث ",[18,7915,7916],{},"patcher"," أحيانًا بسمة قديمة. ربط المفتاح بحالة الفرز يفرض unmount ثم remount نظيفَين عند الانتقال الوحيد الذي قد يهمّ فيه ذلك. سطر واحد فقط. حذفه لم يعضّني حتى الآن، وهذا يعني إما أنه احتياط مضاعف بلا داعٍ، وإما أنه يمنع خطأً بهدوء. لستُ متأكدًا، وقد كففتُ عن محاولة الحسم في أي من الاحتمالين. الكلفة render واحد لكل تبديل فرز.",[26,7919,7921,7924],{"id":7920},"handlevaluebasedonkey-مسارات-بالنقاط-داخل-الصفوف",[18,7922,7923],{},"handleValueBasedOnKey",": مسارات بالنقاط داخل الصفوف",[11,7926,7927,7928,7930,7931,7933],{},"حين لا يوفّر العمود لا ",[18,7929,7104],{}," ولا ",[18,7932,7108],{},"، تعود الخلية إلى renderer افتراضي:",[40,7935,7937],{"className":1893,"code":7936,"language":1895,"meta":45,"style":45},"const children = (cell: IDataTableColumn\u003CT>, record: T) => {\n  const defaultHeadCell = h('span', {\n    class: 'text-[16px] text-secondary font-medium capitalize',\n    innerHTML: typeof cell.title === 'function' ? cell.title(record) : cell.title,\n  });\n  const defaultBodyCell = h('span', {\n    class: 'text-[16px] text-foreground capitalize',\n    innerHTML: handleValueBasedOnKey(cell.key as string, record),\n  });\n  if (props.head) {\n    return cell.headRender ? cell.headRender() : defaultHeadCell;\n  }\n  return cell.bodyRender ? cell.bodyRender(record, props.rowIndex) : defaultBodyCell;\n};\n",[18,7938,7939,7975,7994,8004,8032,8037,8054,8063,8079,8083,8090,8110,8114,8135],{"__ignoreMap":45},[49,7940,7941,7943,7945,7947,7949,7952,7954,7956,7958,7960,7963,7965,7967,7969,7971,7973],{"class":51,"line":52},[49,7942,330],{"class":63},[49,7944,7674],{"class":262},[49,7946,343],{"class":63},[49,7948,2183],{"class":55},[49,7950,7951],{"class":601},"cell",[49,7953,337],{"class":63},[49,7955,6802],{"class":262},[49,7957,56],{"class":55},[49,7959,6807],{"class":262},[49,7961,7962],{"class":55},">, ",[49,7964,7319],{"class":601},[49,7966,337],{"class":63},[49,7968,7003],{"class":262},[49,7970,807],{"class":55},[49,7972,648],{"class":63},[49,7974,1625],{"class":55},[49,7976,7977,7979,7982,7984,7986,7988,7991],{"class":51,"line":82},[49,7978,2603],{"class":63},[49,7980,7981],{"class":333}," defaultHeadCell",[49,7983,343],{"class":63},[49,7985,7529],{"class":262},[49,7987,625],{"class":55},[49,7989,7990],{"class":70},"'span'",[49,7992,7993],{"class":55},", {\n",[49,7995,7996,7999,8002],{"class":51,"line":88},[49,7997,7998],{"class":55},"    class: ",[49,8000,8001],{"class":70},"'text-[16px] text-secondary font-medium capitalize'",[49,8003,360],{"class":55},[49,8005,8006,8009,8011,8014,8016,8018,8020,8023,8025,8027,8029],{"class":51,"line":94},[49,8007,8008],{"class":55},"    innerHTML: ",[49,8010,6713],{"class":63},[49,8012,8013],{"class":55}," cell.title ",[49,8015,654],{"class":63},[49,8017,3818],{"class":70},[49,8019,6723],{"class":63},[49,8021,8022],{"class":55}," cell.",[49,8024,6500],{"class":262},[49,8026,7847],{"class":55},[49,8028,337],{"class":63},[49,8030,8031],{"class":55}," cell.title,\n",[49,8033,8034],{"class":51,"line":363},[49,8035,8036],{"class":55},"  });\n",[49,8038,8039,8041,8044,8046,8048,8050,8052],{"class":51,"line":374},[49,8040,2603],{"class":63},[49,8042,8043],{"class":333}," defaultBodyCell",[49,8045,343],{"class":63},[49,8047,7529],{"class":262},[49,8049,625],{"class":55},[49,8051,7990],{"class":70},[49,8053,7993],{"class":55},[49,8055,8056,8058,8061],{"class":51,"line":385},[49,8057,7998],{"class":55},[49,8059,8060],{"class":70},"'text-[16px] text-foreground capitalize'",[49,8062,360],{"class":55},[49,8064,8065,8067,8069,8072,8074,8076],{"class":51,"line":662},[49,8066,8008],{"class":55},[49,8068,7923],{"class":262},[49,8070,8071],{"class":55},"(cell.key ",[49,8073,2657],{"class":63},[49,8075,607],{"class":333},[49,8077,8078],{"class":55},", record),\n",[49,8080,8081],{"class":51,"line":691},[49,8082,8036],{"class":55},[49,8084,8085,8087],{"class":51,"line":696},[49,8086,2449],{"class":63},[49,8088,8089],{"class":55}," (props.head) {\n",[49,8091,8092,8094,8097,8099,8101,8103,8105,8107],{"class":51,"line":723},[49,8093,2945],{"class":63},[49,8095,8096],{"class":55}," cell.headRender ",[49,8098,758],{"class":63},[49,8100,8022],{"class":55},[49,8102,7104],{"class":262},[49,8104,3615],{"class":55},[49,8106,337],{"class":63},[49,8108,8109],{"class":55}," defaultHeadCell;\n",[49,8111,8112],{"class":51,"line":744},[49,8113,2589],{"class":55},[49,8115,8116,8118,8121,8123,8125,8127,8130,8132],{"class":51,"line":768},[49,8117,2962],{"class":63},[49,8119,8120],{"class":55}," cell.bodyRender ",[49,8122,758],{"class":63},[49,8124,8022],{"class":55},[49,8126,7108],{"class":262},[49,8128,8129],{"class":55},"(record, props.rowIndex) ",[49,8131,337],{"class":63},[49,8133,8134],{"class":55}," defaultBodyCell;\n",[49,8136,8137],{"class":51,"line":779},[49,8138,5759],{"class":55},[11,8140,8141,8142,337],{},"الجزء الذي يستحق التكبير هو ",[18,8143,7923],{},[40,8145,8147],{"className":1893,"code":8146,"language":1895,"meta":45,"style":45},"const handleValueBasedOnKey = (key: string, receivedData: T): unknown => {\n  return key.split('.').reduce((acc: unknown, curr) => {\n    if (acc && typeof acc === 'object') {\n      return (acc as Record\u003Cstring, unknown>)[curr];\n    }\n    return undefined;\n  }, receivedData);\n};\n",[18,8148,8149,8185,8225,8246,8267,8271,8279,8284],{"__ignoreMap":45},[49,8150,8151,8153,8156,8158,8160,8162,8164,8166,8168,8171,8173,8175,8177,8179,8181,8183],{"class":51,"line":52},[49,8152,330],{"class":63},[49,8154,8155],{"class":262}," handleValueBasedOnKey",[49,8157,343],{"class":63},[49,8159,2183],{"class":55},[49,8161,7093],{"class":601},[49,8163,337],{"class":63},[49,8165,607],{"class":333},[49,8167,159],{"class":55},[49,8169,8170],{"class":601},"receivedData",[49,8172,337],{"class":63},[49,8174,7003],{"class":262},[49,8176,2433],{"class":55},[49,8178,337],{"class":63},[49,8180,4446],{"class":333},[49,8182,6288],{"class":63},[49,8184,1625],{"class":55},[49,8186,8187,8189,8192,8195,8197,8200,8202,8205,8207,8210,8212,8214,8216,8219,8221,8223],{"class":51,"line":82},[49,8188,2962],{"class":63},[49,8190,8191],{"class":55}," key.",[49,8193,8194],{"class":262},"split",[49,8196,625],{"class":55},[49,8198,8199],{"class":70},"'.'",[49,8201,1251],{"class":55},[49,8203,8204],{"class":262},"reduce",[49,8206,801],{"class":55},[49,8208,8209],{"class":601},"acc",[49,8211,337],{"class":63},[49,8213,4446],{"class":333},[49,8215,159],{"class":55},[49,8217,8218],{"class":601},"curr",[49,8220,807],{"class":55},[49,8222,648],{"class":63},[49,8224,1625],{"class":55},[49,8226,8227,8229,8232,8234,8236,8239,8241,8244],{"class":51,"line":88},[49,8228,2138],{"class":63},[49,8230,8231],{"class":55}," (acc ",[49,8233,2513],{"class":63},[49,8235,3809],{"class":63},[49,8237,8238],{"class":55}," acc ",[49,8240,654],{"class":63},[49,8242,8243],{"class":70}," 'object'",[49,8245,2149],{"class":55},[49,8247,8248,8250,8252,8254,8256,8258,8260,8262,8264],{"class":51,"line":94},[49,8249,2169],{"class":63},[49,8251,8231],{"class":55},[49,8253,2657],{"class":63},[49,8255,5455],{"class":262},[49,8257,56],{"class":55},[49,8259,1914],{"class":333},[49,8261,159],{"class":55},[49,8263,6871],{"class":333},[49,8265,8266],{"class":55},">)[curr];\n",[49,8268,8269],{"class":51,"line":363},[49,8270,2176],{"class":55},[49,8272,8273,8275,8277],{"class":51,"line":374},[49,8274,2945],{"class":63},[49,8276,5440],{"class":333},[49,8278,319],{"class":55},[49,8280,8281],{"class":51,"line":385},[49,8282,8283],{"class":55},"  }, receivedData);\n",[49,8285,8286],{"class":51,"line":662},[49,8287,5759],{"class":55},[11,8289,8290,8291,8294,8295,8297,8298,8301],{},"عمود بمفتاح ",[18,8292,8293],{},"key: 'user.profile.displayName'"," يسير على النقاط ويسحب القيمة المتشعّبة. هذه الميزة وحدها هي التي تُغني المستهلك عن كتابة دالة ",[18,8296,7108],{}," في 80% من الحالات. معظم الجداول لديها ثلاثة أو أربعة أعمدة كل واحد منها ليس أكثر من ",[18,8299,8300],{},"row.foo.bar","، ومنحُها مسارًا بالنقاط يجعل نهج \"السكيمة كبيانات\" يكسب جولة أخرى.",[11,8303,8304,8305,8308,8309,8312,8313,8315,8316,8318],{},"أستخدم ",[18,8306,8307],{},"innerHTML"," في الـ renderer الافتراضي، وهذا مقصود من أجل العناوين التي تتضمن ",[18,8310,8311],{},"\u003Cbr>"," أو قليلًا من التنسيق، وهو في الوقت نفسه فخّ متوقّع مع أي بيانات يتحكم بها المستخدم. الاتفاق الذي استقرّ عليه فريقي في مراجعات الكود هو: استخدم الـ renderer الافتراضي مع العناوين الثابتة والبيانات المُنقّاة، وأما ما يأتي من المستخدم فاكتب له ",[18,8314,7108],{}," يمرّر البيانات عبر مكوّن فرعي. لم أجد طريقة أنظف للتعبير عن هذه القاعدة داخل نظام الأنواع، ولستُ متأكدًا أنني أريد أصلًا أن أُجبر الجميع على ",[18,8317,7108],{}," لكل خلية.",[26,8320,8322],{"id":8321},"مفاتيح-الصفوف-البديل-حين-لا-يمرّر-أحد-مفتاحًا","مفاتيح الصفوف: البديل حين لا يمرّر أحد مفتاحًا",[11,8324,7450,8325,8327,8328,8330,8331,8333],{},[18,8326,7905],{}," الافتراضي في Vue على الصفوف هو ",[18,8329,7439],{},"، وهو إجابة خاطئة تمامًا لأي شيء يتغير ترتيبه. لتفعل أفضل من ذلك، تحتاج معرّفًا مستقرًّا لكل صف، وبما أن الجدول مُعمَّم على ",[18,8332,6807],{},"، فلا يوجد حقل يمكنك الاعتماد عليه في كل الحالات.",[11,8335,8336],{},"الـ interface واضح:",[40,8338,8340],{"className":1893,"code":8339,"language":1895,"meta":45,"style":45},"type DataTablePropsWithKey\u003CT> = IDataTableProps\u003CT> & {\n  \u002F** Optional stable key extractor; recommended *\u002F\n  rowKey?: (row: T) => string;\n};\n",[18,8341,8342,8371,8376,8400],{"__ignoreMap":45},[49,8343,8344,8346,8349,8351,8353,8355,8357,8360,8362,8364,8366,8369],{"class":51,"line":52},[49,8345,307],{"class":63},[49,8347,8348],{"class":262}," DataTablePropsWithKey",[49,8350,56],{"class":55},[49,8352,6807],{"class":262},[49,8354,5553],{"class":55},[49,8356,67],{"class":63},[49,8358,8359],{"class":262}," IDataTableProps",[49,8361,56],{"class":55},[49,8363,6807],{"class":262},[49,8365,5553],{"class":55},[49,8367,8368],{"class":63},"&",[49,8370,1625],{"class":55},[49,8372,8373],{"class":51,"line":82},[49,8374,8375],{"class":2472},"  \u002F** Optional stable key extractor; recommended *\u002F\n",[49,8377,8378,8381,8383,8385,8388,8390,8392,8394,8396,8398],{"class":51,"line":88},[49,8379,8380],{"class":262},"  rowKey",[49,8382,3946],{"class":63},[49,8384,2183],{"class":55},[49,8386,8387],{"class":601},"row",[49,8389,337],{"class":63},[49,8391,7003],{"class":262},[49,8393,807],{"class":55},[49,8395,648],{"class":63},[49,8397,607],{"class":333},[49,8399,319],{"class":55},[49,8401,8402],{"class":51,"line":94},[49,8403,5759],{"class":55},[11,8405,8406],{},"لكن عمليًا، لا أحد يمرّره. لذلك كان لا بدّ أن يعمل البديل مع صفوف اعتباطية:",[40,8408,8410],{"className":1893,"code":8409,"language":1895,"meta":45,"style":45},"function stableKeyFromObject(obj: Record\u003Cstring, unknown>): string {\n  if ('id' in obj && typeof obj.id !== 'undefined') {\n    return String(obj.id as unknown);\n  }\n  const keys = Object.keys(obj).sort();\n  return keys\n    .map((k) => `${k}:${String(obj[k])}`)\n    .join('|');\n}\n\nfunction getRowKey(row: T): string {\n  if (typeof globalProps.rowKey === 'function') return globalProps.rowKey(row);\n  return stableKeyFromObject(row as unknown as Record\u003Cstring, unknown>);\n}\n",[18,8411,8412,8445,8474,8489,8493,8515,8522,8565,8579,8583,8587,8610,8637,8666],{"__ignoreMap":45},[49,8413,8414,8416,8419,8421,8424,8426,8428,8430,8432,8434,8436,8439,8441,8443],{"class":51,"line":52},[49,8415,2418],{"class":63},[49,8417,8418],{"class":262}," stableKeyFromObject",[49,8420,625],{"class":55},[49,8422,8423],{"class":601},"obj",[49,8425,337],{"class":63},[49,8427,5455],{"class":262},[49,8429,56],{"class":55},[49,8431,1914],{"class":333},[49,8433,159],{"class":55},[49,8435,6871],{"class":333},[49,8437,8438],{"class":55},">)",[49,8440,337],{"class":63},[49,8442,607],{"class":333},[49,8444,1625],{"class":55},[49,8446,8447,8449,8451,8454,8457,8460,8462,8464,8467,8469,8472],{"class":51,"line":82},[49,8448,2449],{"class":63},[49,8450,2183],{"class":55},[49,8452,8453],{"class":70},"'id'",[49,8455,8456],{"class":63}," in",[49,8458,8459],{"class":55}," obj ",[49,8461,2513],{"class":63},[49,8463,3809],{"class":63},[49,8465,8466],{"class":55}," obj.id ",[49,8468,3815],{"class":63},[49,8470,8471],{"class":70}," 'undefined'",[49,8473,2149],{"class":55},[49,8475,8476,8478,8480,8483,8485,8487],{"class":51,"line":88},[49,8477,2945],{"class":63},[49,8479,5814],{"class":262},[49,8481,8482],{"class":55},"(obj.id ",[49,8484,2657],{"class":63},[49,8486,4446],{"class":333},[49,8488,630],{"class":55},[49,8490,8491],{"class":51,"line":94},[49,8492,2589],{"class":55},[49,8494,8495,8497,8500,8502,8504,8507,8510,8513],{"class":51,"line":363},[49,8496,2603],{"class":63},[49,8498,8499],{"class":333}," keys",[49,8501,343],{"class":63},[49,8503,6075],{"class":55},[49,8505,8506],{"class":262},"keys",[49,8508,8509],{"class":55},"(obj).",[49,8511,8512],{"class":262},"sort",[49,8514,3477],{"class":55},[49,8516,8517,8519],{"class":51,"line":374},[49,8518,2962],{"class":63},[49,8520,8521],{"class":55}," keys\n",[49,8523,8524,8527,8529,8531,8534,8536,8538,8541,8543,8546,8548,8550,8552,8555,8557,8560,8562],{"class":51,"line":385},[49,8525,8526],{"class":55},"    .",[49,8528,6233],{"class":262},[49,8530,801],{"class":55},[49,8532,8533],{"class":601},"k",[49,8535,807],{"class":55},[49,8537,648],{"class":63},[49,8539,8540],{"class":70}," `${",[49,8542,8533],{"class":55},[49,8544,8545],{"class":70},"}:${",[49,8547,2217],{"class":262},[49,8549,625],{"class":70},[49,8551,8423],{"class":55},[49,8553,8554],{"class":70},"[",[49,8556,8533],{"class":55},[49,8558,8559],{"class":70},"])",[49,8561,739],{"class":70},[49,8563,8564],{"class":55},")\n",[49,8566,8567,8569,8572,8574,8577],{"class":51,"line":662},[49,8568,8526],{"class":55},[49,8570,8571],{"class":262},"join",[49,8573,625],{"class":55},[49,8575,8576],{"class":70},"'|'",[49,8578,630],{"class":55},[49,8580,8581],{"class":51,"line":691},[49,8582,1719],{"class":55},[49,8584,8585],{"class":51,"line":696},[49,8586,325],{"emptyLinePlaceholder":324},[49,8588,8589,8591,8594,8596,8598,8600,8602,8604,8606,8608],{"class":51,"line":723},[49,8590,2418],{"class":63},[49,8592,8593],{"class":262}," getRowKey",[49,8595,625],{"class":55},[49,8597,8387],{"class":601},[49,8599,337],{"class":63},[49,8601,7003],{"class":262},[49,8603,2433],{"class":55},[49,8605,337],{"class":63},[49,8607,607],{"class":333},[49,8609,1625],{"class":55},[49,8611,8612,8614,8616,8618,8621,8623,8625,8627,8629,8632,8634],{"class":51,"line":744},[49,8613,2449],{"class":63},[49,8615,2183],{"class":55},[49,8617,6713],{"class":63},[49,8619,8620],{"class":55}," globalProps.rowKey ",[49,8622,654],{"class":63},[49,8624,3818],{"class":70},[49,8626,807],{"class":55},[49,8628,2197],{"class":63},[49,8630,8631],{"class":55}," globalProps.",[49,8633,6505],{"class":262},[49,8635,8636],{"class":55},"(row);\n",[49,8638,8639,8641,8643,8646,8648,8650,8653,8655,8657,8659,8661,8663],{"class":51,"line":768},[49,8640,2962],{"class":63},[49,8642,8418],{"class":262},[49,8644,8645],{"class":55},"(row ",[49,8647,2657],{"class":63},[49,8649,4446],{"class":333},[49,8651,8652],{"class":63}," as",[49,8654,5455],{"class":262},[49,8656,56],{"class":55},[49,8658,1914],{"class":333},[49,8660,159],{"class":55},[49,8662,6871],{"class":333},[49,8664,8665],{"class":55},">);\n",[49,8667,8668],{"class":51,"line":779},[49,8669,1719],{"class":55},[11,8671,8672],{},"ثلاثة تفاصيل صغيرة، وكل واحد منها يستحق وزنه:",[8674,8675,8676,8693,8707],"ul",{},[3673,8677,8678,8685,8686,8688,8689,8692],{},[36,8679,8680,8681,8684],{},"اختصار ",[18,8682,8683],{},"id"," أولًا."," أغلب سجلات الـ backend فيها حقل ",[18,8687,8683],{},". ",[18,8690,8691],{},"String(obj.id)"," يتعامل مع الأرقام والسلاسل و BigInt دون تفرّع. التحويل بالنوع كذبة في نظام الأنواع وصدق وقت التشغيل.",[3673,8694,8695,8698,8699,8702,8703,8706],{},[36,8696,8697],{},"رتّب المفاتيح قبل اشتقاق البصمة."," ",[18,8700,8701],{},"Object.keys()"," يعيد ترتيب الإدخال. في أغلب الكائنات يكون هذا الترتيب هو نفسه في كل مرة، لكن \"أغلب\" ليست \"كل\". كائن أعيد بناؤه من ",[18,8704,8705],{},"JSON.parse"," لن يتفق بالضرورة مع كائن كُتب مباشرة كـ object literal بالمفاتيح نفسها. الترتيب يقضي على هذا الصنف من الأخطاء دفعة واحدة.",[3673,8708,8709,8698,8718,8720,8721,8723],{},[36,8710,8711,8714,8715,527],{},[18,8712,8713],{},"String()"," بدل ",[18,8716,8717],{},"JSON.stringify",[18,8719,8713],{}," لا يرمي استثناءً أبدًا، بينما ",[18,8722,8717],{}," يرمي على المراجع الدائرية (وهي تحدث حين تكون مُستهترًا مع Pinia). الهدف هنا مفتاح ثابت، لا مُسلسِل كامل.",[11,8725,8726,8727,8730,8731,8734,8735,8737,8738,8741],{},"هذا المفتاح يُستخدم في مطابقة Vue ",[7077,8728,8729],{},"و"," في التحديد معًا. وهذه هي الثابتة التي تهمّ. لو استخدم الاثنان مفهومَي هوية مختلفَين (",[18,8732,8733],{},":key=\"i\""," وتحديد يستخدم ",[18,8736,654],{},")، فقد تُسقط عملية فرز واحدة مربعات الاختيار مع بقاء البيانات نفسها. استخدام ",[18,8739,8740],{},"getRowKey"," في الموضعَين يضمن أن الاثنين لا يفترقان.",[26,8743,8745,8746],{"id":8744},"التحديد-على-شكل-mapstring-t","التحديد على شكل ",[18,8747,8748],{},"Map\u003Cstring, T>",[11,8750,8751,8752,8755,8756,8759],{},"لا توجد حالة تحديد منفصلة. المكوّن يحتفظ فقط بـ ",[18,8753,8754],{},"localSelectedItems"," (وهي ",[18,8757,8758],{},"shallowRef\u003CT[]>",")، ويشتق منها Map مفهرسة بالمفتاح:",[40,8761,8763],{"className":1893,"code":8762,"language":1895,"meta":45,"style":45},"const hashedSelectedItems = computed(() => {\n  const map = new Map\u003Cstring, T>();\n  localSelectedItems.value.forEach((x) => map.set(getRowKey(x), x));\n  return map;\n});\n",[18,8764,8765,8782,8807,8837,8844],{"__ignoreMap":45},[49,8766,8767,8769,8772,8774,8776,8778,8780],{"class":51,"line":52},[49,8768,330],{"class":63},[49,8770,8771],{"class":333}," hashedSelectedItems",[49,8773,343],{"class":63},[49,8775,642],{"class":262},[49,8777,645],{"class":55},[49,8779,648],{"class":63},[49,8781,1625],{"class":55},[49,8783,8784,8786,8789,8791,8793,8796,8798,8800,8802,8804],{"class":51,"line":82},[49,8785,2603],{"class":63},[49,8787,8788],{"class":333}," map",[49,8790,343],{"class":63},[49,8792,3838],{"class":63},[49,8794,8795],{"class":262}," Map",[49,8797,56],{"class":55},[49,8799,1914],{"class":333},[49,8801,159],{"class":55},[49,8803,6807],{"class":262},[49,8805,8806],{"class":55},">();\n",[49,8808,8809,8812,8815,8817,8820,8822,8824,8827,8830,8832,8834],{"class":51,"line":88},[49,8810,8811],{"class":55},"  localSelectedItems.value.",[49,8813,8814],{"class":262},"forEach",[49,8816,801],{"class":55},[49,8818,8819],{"class":601},"x",[49,8821,807],{"class":55},[49,8823,648],{"class":63},[49,8825,8826],{"class":55}," map.",[49,8828,8829],{"class":262},"set",[49,8831,625],{"class":55},[49,8833,8740],{"class":262},[49,8835,8836],{"class":55},"(x), x));\n",[49,8838,8839,8841],{"class":51,"line":94},[49,8840,2962],{"class":63},[49,8842,8843],{"class":55}," map;\n",[49,8845,8846],{"class":51,"line":363},[49,8847,388],{"class":55},[11,8849,8850],{},"مربع اختيار رأس عمود التحديد يفحص \"هل كل العناصر محدَّدة\" بمسح واحد للمصفوفة:",[40,8852,8854],{"className":1893,"code":8853,"language":1895,"meta":45,"style":45},"const isAllDataSelected = computed(() => {\n  const atData = localItems.value.filter((item) =>\n    hashedSelectedItems.value.has(getRowKey(item))\n  );\n  return atData.length === localItems.value.length && localItems.value.length > 0;\n});\n",[18,8855,8856,8873,8897,8912,8916,8946],{"__ignoreMap":45},[49,8857,8858,8860,8863,8865,8867,8869,8871],{"class":51,"line":52},[49,8859,330],{"class":63},[49,8861,8862],{"class":333}," isAllDataSelected",[49,8864,343],{"class":63},[49,8866,642],{"class":262},[49,8868,645],{"class":55},[49,8870,648],{"class":63},[49,8872,1625],{"class":55},[49,8874,8875,8877,8880,8882,8885,8888,8890,8892,8894],{"class":51,"line":82},[49,8876,2603],{"class":63},[49,8878,8879],{"class":333}," atData",[49,8881,343],{"class":63},[49,8883,8884],{"class":55}," localItems.value.",[49,8886,8887],{"class":262},"filter",[49,8889,801],{"class":55},[49,8891,5026],{"class":601},[49,8893,807],{"class":55},[49,8895,8896],{"class":63},"=>\n",[49,8898,8899,8902,8905,8907,8909],{"class":51,"line":88},[49,8900,8901],{"class":55},"    hashedSelectedItems.value.",[49,8903,8904],{"class":262},"has",[49,8906,625],{"class":55},[49,8908,8740],{"class":262},[49,8910,8911],{"class":55},"(item))\n",[49,8913,8914],{"class":51,"line":94},[49,8915,7696],{"class":55},[49,8917,8918,8920,8923,8925,8927,8929,8931,8934,8936,8938,8941,8944],{"class":51,"line":363},[49,8919,2962],{"class":63},[49,8921,8922],{"class":55}," atData.",[49,8924,2626],{"class":333},[49,8926,2913],{"class":63},[49,8928,8884],{"class":55},[49,8930,2626],{"class":333},[49,8932,8933],{"class":63}," &&",[49,8935,8884],{"class":55},[49,8937,2626],{"class":333},[49,8939,8940],{"class":63}," >",[49,8942,8943],{"class":333}," 0",[49,8945,319],{"class":55},[49,8947,8948],{"class":51,"line":374},[49,8949,388],{"class":55},[11,8951,8952],{},"هذا O(n) على العناصر المرئية، وهي أصلًا O(n) بالنسبة إلى الـ render نفسه. لا شيء يمكن فعله لتحسين هذا. أما الفحص لكل صف على حدة فهو O(1):",[40,8954,8956],{"className":1893,"code":8955,"language":1895,"meta":45,"style":45},"modelValue: hashedSelectedItems.value.has(getRowKey(rowData)),\n",[18,8957,8958],{"__ignoreMap":45},[49,8959,8960,8963,8966,8968,8970,8972],{"class":51,"line":52},[49,8961,8962],{"class":262},"modelValue",[49,8964,8965],{"class":55},": hashedSelectedItems.value.",[49,8967,8904],{"class":262},[49,8969,625],{"class":55},[49,8971,8740],{"class":262},[49,8973,8974],{"class":55},"(rowData)),\n",[11,8976,8977,8978,8981],{},"بدون هذه الـ Map المحسوبة، سيقوم كل مربع اختيار بـ ",[18,8979,8980],{},"localSelectedItems.value.find(...)"," عند كل render. جدول من 100 صف مع 100 عنصر محدَّد كان سينفّذ 10,000 مقارنة في كل إعادة render. الـ Map تحوّل ذلك إلى 100 بحث بزمن ثابت. لا تلاحظ الفرق قبل أن تُفجّر جدولًا ببيانات حقيقية، وعند تلك اللحظة تحفظ النمط وتتوقف عن كتابة النسخة الساذجة.",[11,8983,8984,8985,8988],{},"عمود التحديد نفسه يُبنى عند الطلب. المستهلك إما يصرّح به عمودًا (",[18,8986,8987],{},"type: 'selection'",") وإما يتركه:",[40,8990,8992],{"className":1893,"code":8991,"language":1895,"meta":45,"style":45},"if (hasSelectColumn) {\n  const selectionColumn = handleSelectionColumn();\n  _columns = _columns.filter((col) => col.type !== 'selection');\n  _columns.unshift(selectionColumn);\n}\n",[18,8993,8994,9001,9015,9045,9056],{"__ignoreMap":45},[49,8995,8996,8998],{"class":51,"line":52},[49,8997,3249],{"class":63},[49,8999,9000],{"class":55}," (hasSelectColumn) {\n",[49,9002,9003,9005,9008,9010,9013],{"class":51,"line":82},[49,9004,2603],{"class":63},[49,9006,9007],{"class":333}," selectionColumn",[49,9009,343],{"class":63},[49,9011,9012],{"class":262}," handleSelectionColumn",[49,9014,3477],{"class":55},[49,9016,9017,9020,9022,9025,9027,9029,9032,9034,9036,9039,9041,9043],{"class":51,"line":88},[49,9018,9019],{"class":55},"  _columns ",[49,9021,67],{"class":63},[49,9023,9024],{"class":55}," _columns.",[49,9026,8887],{"class":262},[49,9028,801],{"class":55},[49,9030,9031],{"class":601},"col",[49,9033,807],{"class":55},[49,9035,648],{"class":63},[49,9037,9038],{"class":55}," col.type ",[49,9040,3815],{"class":63},[49,9042,6902],{"class":70},[49,9044,630],{"class":55},[49,9046,9047,9050,9053],{"class":51,"line":94},[49,9048,9049],{"class":55},"  _columns.",[49,9051,9052],{"class":262},"unshift",[49,9054,9055],{"class":55},"(selectionColumn);\n",[49,9057,9058],{"class":51,"line":363},[49,9059,1719],{"class":55},[11,9061,9062,9063,9065,9066,9069,9070,527],{},"عمود ",[18,9064,8987],{}," القادم من المستهلك، إن وُجد، يُستبدَل بواحد يبنيه المكوّن داخليًا ويُثبَّت في الموضع 0. نسخة المستهلك مجرد ",[7077,9067,9068],{},"علامة"," تعني \"أريد تحديدًا في هذا الجدول\"، والمكوّن نفسه يتولى كتابة الـ renderer الفعلي. بهذا يحصل المستهلك على واجهة \"التحديد عمود\" دون أن يُطلب منه أن يعرف كيف يُبنى مربع الاختيار أو كيف تُوصَّل حالة ",[18,9071,9072],{},"indeterminate",[26,9074,9076,9077,9080],{"id":9075},"الإجراءات-وmarkraw-الذي-أثبت-جدواه","الإجراءات، و",[18,9078,9079],{},"markRaw"," الذي أثبت جدواه",[11,9082,9083,9084,9087],{},"الإجراءات تعمل بالطريقة نفسها تمامًا. المستهلك يصرّح بعمود إجراءات (أو يُلحقه المكوّن تلقائيًا إذا كان ",[18,9085,9086],{},"actions.length > 0",")، والـ renderer يُبنى داخليًا:",[40,9089,9091],{"className":1893,"code":9090,"language":1895,"meta":45,"style":45},"const actionsComponent: IDataTableRenderFunction\u003CT> = (rowData, index) =>\n  h(markRaw(AActionGroup), {\n    actions: localActions.value as IAction\u003Cunknown>[],\n    record: rowData,\n    popover: globalProps.popover,\n    maxMainCount: getVisibleLength(rowData),\n    onUpdateAction: (action) =>\n      emit('updateAction', { action, row: rowData, rowIndex: index }),\n  });\n",[18,9092,9093,9126,9138,9155,9160,9165,9176,9191,9204],{"__ignoreMap":45},[49,9094,9095,9097,9100,9102,9104,9106,9108,9110,9112,9114,9117,9119,9122,9124],{"class":51,"line":52},[49,9096,330],{"class":63},[49,9098,9099],{"class":262}," actionsComponent",[49,9101,337],{"class":63},[49,9103,6965],{"class":262},[49,9105,56],{"class":55},[49,9107,6807],{"class":262},[49,9109,5553],{"class":55},[49,9111,67],{"class":63},[49,9113,2183],{"class":55},[49,9115,9116],{"class":601},"rowData",[49,9118,159],{"class":55},[49,9120,9121],{"class":601},"index",[49,9123,807],{"class":55},[49,9125,8896],{"class":63},[49,9127,9128,9131,9133,9135],{"class":51,"line":82},[49,9129,9130],{"class":262},"  h",[49,9132,625],{"class":55},[49,9134,9079],{"class":262},[49,9136,9137],{"class":55},"(AActionGroup), {\n",[49,9139,9140,9143,9145,9148,9150,9152],{"class":51,"line":88},[49,9141,9142],{"class":55},"    actions: localActions.value ",[49,9144,2657],{"class":63},[49,9146,9147],{"class":262}," IAction",[49,9149,56],{"class":55},[49,9151,6871],{"class":333},[49,9153,9154],{"class":55},">[],\n",[49,9156,9157],{"class":51,"line":94},[49,9158,9159],{"class":55},"    record: rowData,\n",[49,9161,9162],{"class":51,"line":363},[49,9163,9164],{"class":55},"    popover: globalProps.popover,\n",[49,9166,9167,9170,9173],{"class":51,"line":374},[49,9168,9169],{"class":55},"    maxMainCount: ",[49,9171,9172],{"class":262},"getVisibleLength",[49,9174,9175],{"class":55},"(rowData),\n",[49,9177,9178,9181,9184,9187,9189],{"class":51,"line":385},[49,9179,9180],{"class":262},"    onUpdateAction",[49,9182,9183],{"class":55},": (",[49,9185,9186],{"class":601},"action",[49,9188,807],{"class":55},[49,9190,8896],{"class":63},[49,9192,9193,9196,9198,9201],{"class":51,"line":662},[49,9194,9195],{"class":262},"      emit",[49,9197,625],{"class":55},[49,9199,9200],{"class":70},"'updateAction'",[49,9202,9203],{"class":55},", { action, row: rowData, rowIndex: index }),\n",[49,9205,9206],{"class":51,"line":691},[49,9207,8036],{"class":55},[11,9209,9210,9211,527],{},"السطر الذي أريد أن أتوقف عنده هو ",[18,9212,9213],{},"markRaw(AActionGroup)",[11,9215,9216,9217,9219,9220,9222,9223,3728,9226,9229],{},"حين تمرّر تعريف مكوّن إلى ",[18,9218,7483],{},"، فإن Vue يلمسه أثناء الـ render. وبدون ",[18,9221,9079],{},"، لو مرّ هذا التعريف يومًا عبر نظام التفاعلية في Vue، سواء عبر ",[18,9224,9225],{},"ref",[18,9227,9228],{},"reactive"," أو حتى prop، فإنه يُلفّ في Proxy. تعريفات المكوّنات ليست مصمَّمة أصلًا لتكون تفاعلية، هي بيانات وصفية ثابتة. تغليفها لا يكسر شيئًا، لكنه يجعل كل قراءة لخاصية تمرّ عبر handler يقوم بفحص تتبّع تفاعلية لا يُثمر أبدًا.",[11,9231,9232,9233,9236,9237,9239,9240,9243],{},"دالة الـ render أعلاه تعمل مرة لكل صف. في جدول من 500 صف، يلمس أول render تعريف ",[18,9234,9235],{},"AActionGroup"," خمسمئة مرة. ",[18,9238,9079],{}," يضع علامة ",[18,9241,9242],{},"__v_skip"," على الكائن مرة واحدة، وكل render لاحق يتخطى مسار الـ Proxy تمامًا.",[11,9245,9246,9247,9249,9250,9253,9254,9256],{},"كلفة ",[18,9248,9079],{}," دائمة، لا يمكن التراجع عنها. وهذا مقبول تمامًا مع تعريف مكوّن، فهو import ثابت في نهاية المطاف. لو أردتَ تبديل مكوّن الإجراءات وقت التشغيل، فالتبديل يحصل في طبقة مختلفة، أي في اختيار ",[7077,9251,9252],{},"أي"," مكوّن تمرّره إلى ",[18,9255,9079],{},"، لا بأن تجعل المرجع نفسه تفاعليًا.",[11,9258,9259,9260,9262,9263,9265,9266,9269,9270,9272],{},"هناك أيضًا تنبيه في وضع التطوير نصّه \"Vue received a Component that was made a reactive object\"، يظهر حين يتسلل مكوّن تفاعلي إلى ",[18,9261,7483],{},". اصطدمتُ به في جدول من 1200 صف كان يمرَّر فيه ",[18,9264,9235],{}," عبر ",[18,9267,9268],{},"shallowRef"," لأغراض الـ theming. إضافة ",[18,9271,9079],{}," أسكتت التنبيه وخفّضت زمن أول رسم بشكل ملحوظ.",[11,9274,9275],{},"عدد الإجراءات الظاهرة يمكن أن يكون رقمًا أو دالة على الصف:",[40,9277,9279],{"className":1893,"code":9278,"language":1895,"meta":45,"style":45},"const getVisibleLength = (rowData: T) =>\n  typeof globalProps.visibleActionLength === 'function'\n    ? globalProps.visibleActionLength(rowData)\n    : globalProps.visibleActionLength;\n",[18,9280,9281,9302,9315,9328],{"__ignoreMap":45},[49,9282,9283,9285,9288,9290,9292,9294,9296,9298,9300],{"class":51,"line":52},[49,9284,330],{"class":63},[49,9286,9287],{"class":262}," getVisibleLength",[49,9289,343],{"class":63},[49,9291,2183],{"class":55},[49,9293,9116],{"class":601},[49,9295,337],{"class":63},[49,9297,7003],{"class":262},[49,9299,807],{"class":55},[49,9301,8896],{"class":63},[49,9303,9304,9307,9310,9312],{"class":51,"line":82},[49,9305,9306],{"class":63},"  typeof",[49,9308,9309],{"class":55}," globalProps.visibleActionLength ",[49,9311,654],{"class":63},[49,9313,9314],{"class":70}," 'function'\n",[49,9316,9317,9320,9322,9325],{"class":51,"line":88},[49,9318,9319],{"class":63},"    ?",[49,9321,8631],{"class":55},[49,9323,9324],{"class":262},"visibleActionLength",[49,9326,9327],{"class":55},"(rowData)\n",[49,9329,9330,9333],{"class":51,"line":94},[49,9331,9332],{"class":63},"    :",[49,9334,9335],{"class":55}," globalProps.visibleActionLength;\n",[11,9337,9338],{},"وهذا هو السبيل الوحيد لتنفيذ \"اعرض 3 إجراءات للـ admin وإجراءً واحدًا للمستخدم العادي\" دون أن نبني عمود إجراءات ثانيًا.",[26,9340,9342],{"id":9341},"الفرز-دورة-من-ثلاث-نقرات-محلية-أو-عن-بُعد","الفرز: دورة من ثلاث نقرات، محلية أو عن بُعد",[11,9344,9345],{},"حالة الفرز كائن ذو قيمتَين منطقيتَين:",[40,9347,9349],{"className":1893,"code":9348,"language":1895,"meta":45,"style":45},"export type ISortValue = { applied: boolean; revert: boolean };\n",[18,9350,9351],{"__ignoreMap":45},[49,9352,9353,9355,9358,9360,9362,9364,9367,9369,9371,9373,9376,9378,9380],{"class":51,"line":52},[49,9354,3341],{"class":63},[49,9356,9357],{"class":63}," type",[49,9359,7030],{"class":262},[49,9361,343],{"class":63},[49,9363,701],{"class":55},[49,9365,9366],{"class":601},"applied",[49,9368,337],{"class":63},[49,9370,4678],{"class":333},[49,9372,3951],{"class":55},[49,9374,9375],{"class":601},"revert",[49,9377,337],{"class":63},[49,9379,4678],{"class":333},[49,9381,3960],{"class":55},[11,9383,9384,9385,9388],{},"وليست ",[18,9386,9387],{},"'asc' | 'desc' | null",". جرّبتُ النصوص أولًا، لكنها كانت تفقد باستمرار حالة \"لم يُطبَّق\"، لأن القيم الزائفة من النوع الخاطئ لا تتعايش بلطف مع TypeScript الصارم.",[11,9390,9391],{},"التبديل يمرّ بثلاث حالات:",[40,9393,9395],{"className":1893,"code":9394,"language":1895,"meta":45,"style":45},"function handleToggleSortButton(sorterValue: ISortValue): ISortValue {\n  if (!sorterValue.applied) {\n    sorterValue.applied = true;\n  } else if (!sorterValue.revert) {\n    sorterValue.revert = true;\n  } else {\n    sorterValue.applied = false;\n    sorterValue.revert = false;\n  }\n  return sorterValue;\n}\n",[18,9396,9397,9421,9432,9443,9460,9471,9479,9489,9499,9503,9510],{"__ignoreMap":45},[49,9398,9399,9401,9404,9406,9409,9411,9413,9415,9417,9419],{"class":51,"line":52},[49,9400,2418],{"class":63},[49,9402,9403],{"class":262}," handleToggleSortButton",[49,9405,625],{"class":55},[49,9407,9408],{"class":601},"sorterValue",[49,9410,337],{"class":63},[49,9412,7030],{"class":262},[49,9414,2433],{"class":55},[49,9416,337],{"class":63},[49,9418,7030],{"class":262},[49,9420,1625],{"class":55},[49,9422,9423,9425,9427,9429],{"class":51,"line":82},[49,9424,2449],{"class":63},[49,9426,2183],{"class":55},[49,9428,2454],{"class":63},[49,9430,9431],{"class":55},"sorterValue.applied) {\n",[49,9433,9434,9437,9439,9441],{"class":51,"line":88},[49,9435,9436],{"class":55},"    sorterValue.applied ",[49,9438,67],{"class":63},[49,9440,3174],{"class":333},[49,9442,319],{"class":55},[49,9444,9445,9447,9450,9453,9455,9457],{"class":51,"line":94},[49,9446,2574],{"class":55},[49,9448,9449],{"class":63},"else",[49,9451,9452],{"class":63}," if",[49,9454,2183],{"class":55},[49,9456,2454],{"class":63},[49,9458,9459],{"class":55},"sorterValue.revert) {\n",[49,9461,9462,9465,9467,9469],{"class":51,"line":363},[49,9463,9464],{"class":55},"    sorterValue.revert ",[49,9466,67],{"class":63},[49,9468,3174],{"class":333},[49,9470,319],{"class":55},[49,9472,9473,9475,9477],{"class":51,"line":374},[49,9474,2574],{"class":55},[49,9476,9449],{"class":63},[49,9478,1625],{"class":55},[49,9480,9481,9483,9485,9487],{"class":51,"line":385},[49,9482,9436],{"class":55},[49,9484,67],{"class":63},[49,9486,3299],{"class":333},[49,9488,319],{"class":55},[49,9490,9491,9493,9495,9497],{"class":51,"line":662},[49,9492,9464],{"class":55},[49,9494,67],{"class":63},[49,9496,3299],{"class":333},[49,9498,319],{"class":55},[49,9500,9501],{"class":51,"line":691},[49,9502,2589],{"class":55},[49,9504,9505,9507],{"class":51,"line":696},[49,9506,2962],{"class":63},[49,9508,9509],{"class":55}," sorterValue;\n",[49,9511,9512],{"class":51,"line":723},[49,9513,1719],{"class":55},[11,9515,9516],{},"نقرة أولى: تصاعدي. نقرة ثانية: تنازلي. نقرة ثالثة: عودة إلى الترتيب الأصلي. المستخدمون يتوقّعون هذا في كل جدول تعاملوا معه، ويُفاجَؤون دائمًا حين لا يجدون نقرة \"إلغاء الفرز\".",[11,9518,9519],{},"الفرز الفعلي له مساران: عن بُعد ومحلي.",[40,9521,9523],{"className":1893,"code":9522,"language":1895,"meta":45,"style":45},"function sortCell(columnIndex: number) {\n  const column = localColumns.value[columnIndex]!;\n  const sorterFunction = column.sorter;\n  const sorterValue = handleToggleSortButton(column.sortValue!);\n\n  if (globalProps.remote) {\n    emit('updateSorter', { column, sorterValue, rowIndex: columnIndex });\n    return;\n  }\n\n  if (!sorterValue.applied && !sorterValue.revert) {\n    localItems.value = _.cloneDeep(globalProps.items);\n    emit('updateSorter', { column, sorterValue, rowIndex: columnIndex });\n    return;\n  }\n\n  localItems.value = [...localItems.value].sort((a, b) => {\n    const result = sorterFunction!(a, b);\n    return sorterValue.revert ? -result : result;\n  });\n  emit('updateSorter', { column, sorterValue, rowIndex: columnIndex });\n}\n",[18,9524,9525,9543,9559,9571,9589,9593,9600,9613,9619,9623,9627,9645,9661,9671,9677,9681,9685,9716,9732,9752,9756,9767],{"__ignoreMap":45},[49,9526,9527,9529,9532,9534,9537,9539,9541],{"class":51,"line":52},[49,9528,2418],{"class":63},[49,9530,9531],{"class":262}," sortCell",[49,9533,625],{"class":55},[49,9535,9536],{"class":601},"columnIndex",[49,9538,337],{"class":63},[49,9540,3775],{"class":333},[49,9542,2149],{"class":55},[49,9544,9545,9547,9550,9552,9555,9557],{"class":51,"line":82},[49,9546,2603],{"class":63},[49,9548,9549],{"class":333}," column",[49,9551,343],{"class":63},[49,9553,9554],{"class":55}," localColumns.value[columnIndex]",[49,9556,2454],{"class":63},[49,9558,319],{"class":55},[49,9560,9561,9563,9566,9568],{"class":51,"line":88},[49,9562,2603],{"class":63},[49,9564,9565],{"class":333}," sorterFunction",[49,9567,343],{"class":63},[49,9569,9570],{"class":55}," column.sorter;\n",[49,9572,9573,9575,9578,9580,9582,9585,9587],{"class":51,"line":94},[49,9574,2603],{"class":63},[49,9576,9577],{"class":333}," sorterValue",[49,9579,343],{"class":63},[49,9581,9403],{"class":262},[49,9583,9584],{"class":55},"(column.sortValue",[49,9586,2454],{"class":63},[49,9588,630],{"class":55},[49,9590,9591],{"class":51,"line":363},[49,9592,325],{"emptyLinePlaceholder":324},[49,9594,9595,9597],{"class":51,"line":374},[49,9596,2449],{"class":63},[49,9598,9599],{"class":55}," (globalProps.remote) {\n",[49,9601,9602,9605,9607,9610],{"class":51,"line":385},[49,9603,9604],{"class":262},"    emit",[49,9606,625],{"class":55},[49,9608,9609],{"class":70},"'updateSorter'",[49,9611,9612],{"class":55},", { column, sorterValue, rowIndex: columnIndex });\n",[49,9614,9615,9617],{"class":51,"line":662},[49,9616,2945],{"class":63},[49,9618,319],{"class":55},[49,9620,9621],{"class":51,"line":691},[49,9622,2589],{"class":55},[49,9624,9625],{"class":51,"line":696},[49,9626,325],{"emptyLinePlaceholder":324},[49,9628,9629,9631,9633,9635,9638,9640,9643],{"class":51,"line":723},[49,9630,2449],{"class":63},[49,9632,2183],{"class":55},[49,9634,2454],{"class":63},[49,9636,9637],{"class":55},"sorterValue.applied ",[49,9639,2513],{"class":63},[49,9641,9642],{"class":63}," !",[49,9644,9459],{"class":55},[49,9646,9647,9650,9652,9655,9658],{"class":51,"line":744},[49,9648,9649],{"class":55},"    localItems.value ",[49,9651,67],{"class":63},[49,9653,9654],{"class":55}," _.",[49,9656,9657],{"class":262},"cloneDeep",[49,9659,9660],{"class":55},"(globalProps.items);\n",[49,9662,9663,9665,9667,9669],{"class":51,"line":768},[49,9664,9604],{"class":262},[49,9666,625],{"class":55},[49,9668,9609],{"class":70},[49,9670,9612],{"class":55},[49,9672,9673,9675],{"class":51,"line":779},[49,9674,2945],{"class":63},[49,9676,319],{"class":55},[49,9678,9679],{"class":51,"line":784},[49,9680,2589],{"class":55},[49,9682,9683],{"class":51,"line":789},[49,9684,325],{"emptyLinePlaceholder":324},[49,9686,9687,9690,9692,9694,9697,9700,9702,9704,9706,9708,9710,9712,9714],{"class":51,"line":820},[49,9688,9689],{"class":55},"  localItems.value ",[49,9691,67],{"class":63},[49,9693,7654],{"class":55},[49,9695,9696],{"class":63},"...",[49,9698,9699],{"class":55},"localItems.value].",[49,9701,8512],{"class":262},[49,9703,801],{"class":55},[49,9705,6580],{"class":601},[49,9707,159],{"class":55},[49,9709,6585],{"class":601},[49,9711,807],{"class":55},[49,9713,648],{"class":63},[49,9715,1625],{"class":55},[49,9717,9718,9720,9723,9725,9727,9729],{"class":51,"line":829},[49,9719,2204],{"class":63},[49,9721,9722],{"class":333}," result",[49,9724,343],{"class":63},[49,9726,9565],{"class":262},[49,9728,2454],{"class":63},[49,9730,9731],{"class":55},"(a, b);\n",[49,9733,9734,9736,9739,9741,9744,9747,9749],{"class":51,"line":834},[49,9735,2945],{"class":63},[49,9737,9738],{"class":55}," sorterValue.revert ",[49,9740,758],{"class":63},[49,9742,9743],{"class":63}," -",[49,9745,9746],{"class":55},"result ",[49,9748,337],{"class":63},[49,9750,9751],{"class":55}," result;\n",[49,9753,9754],{"class":51,"line":844},[49,9755,8036],{"class":55},[49,9757,9758,9761,9763,9765],{"class":51,"line":861},[49,9759,9760],{"class":262},"  emit",[49,9762,625],{"class":55},[49,9764,9609],{"class":70},[49,9766,9612],{"class":55},[49,9768,9769],{"class":51,"line":888},[49,9770,1719],{"class":55},[11,9772,9773,9774,9777],{},"الوضع البعيد يُصدر emit ويترك للأب تولّي الترقيم والفرز على مستوى الـ backend. الوضع المحلي يُشغّل الـ sorter في الذاكرة، مع تفصيلة واحدة: حين يعود المستخدم إلى حالة \"غير مفروز\"، يُعاد استنساخ العناصر المحلية من ",[18,9775,9776],{},"globalProps.items",". هذا يستعيد الترتيب الأصلي دون أن نحتاج إلى تذكّره أصلًا.",[11,9779,9780],{},"الـ emit يُطلَق في الحالات الثلاث. الـ backend البعيدة تحتاجه لإعادة الجلب، والمستهلك المحلي يستطيع استخدامه لحفظ اختيار المستخدم في query param داخل الـ URL. إطلاق الـ emit بلا شروط أرخص من التفرّع، والمستهلك يستطيع تجاهله ببساطة.",[26,9782,9784,9785,394,9787,394,9789],{"id":9783},"التفاعلية-shallowref-وclonedeep-وdebouncedwatch","التفاعلية: ",[18,9786,9268],{},[18,9788,9657],{},[18,9790,9791],{},"debouncedWatch",[11,9793,9794,9795,394,9798,394,9801,394,9804,9806,9807,337],{},"المكوّن لا يراقب بيانات المستهلك مراقبة عميقة. ",[18,9796,9797],{},"localItems",[18,9799,9800],{},"localColumns",[18,9802,9803],{},"localActions",[18,9805,8754],{},"، جميعها ",[18,9808,9268],{},[40,9810,9812],{"className":1893,"code":9811,"language":1895,"meta":45,"style":45},"const localItems = shallowRef\u003CT[]>(_.cloneDeep(globalProps.items));\nconst localColumns = shallowRef\u003CIDataTableColumn\u003CT>[]>([]);\nconst localActions = shallowRef\u003CIAction\u003CT>[]>(_.cloneDeep(globalProps.actions));\nconst localSelectedItems = shallowRef\u003CT[]>(_.cloneDeep(globalProps.selectedItems));\n",[18,9813,9814,9838,9861,9889],{"__ignoreMap":45},[49,9815,9816,9818,9821,9823,9826,9828,9830,9833,9835],{"class":51,"line":52},[49,9817,330],{"class":63},[49,9819,9820],{"class":333}," localItems",[49,9822,343],{"class":63},[49,9824,9825],{"class":262}," shallowRef",[49,9827,56],{"class":55},[49,9829,6807],{"class":262},[49,9831,9832],{"class":55},"[]>(_.",[49,9834,9657],{"class":262},[49,9836,9837],{"class":55},"(globalProps.items));\n",[49,9839,9840,9842,9845,9847,9849,9851,9854,9856,9858],{"class":51,"line":82},[49,9841,330],{"class":63},[49,9843,9844],{"class":333}," localColumns",[49,9846,343],{"class":63},[49,9848,9825],{"class":262},[49,9850,56],{"class":55},[49,9852,9853],{"class":262},"IDataTableColumn",[49,9855,56],{"class":55},[49,9857,6807],{"class":262},[49,9859,9860],{"class":55},">[]>([]);\n",[49,9862,9863,9865,9868,9870,9872,9874,9877,9879,9881,9884,9886],{"class":51,"line":88},[49,9864,330],{"class":63},[49,9866,9867],{"class":333}," localActions",[49,9869,343],{"class":63},[49,9871,9825],{"class":262},[49,9873,56],{"class":55},[49,9875,9876],{"class":262},"IAction",[49,9878,56],{"class":55},[49,9880,6807],{"class":262},[49,9882,9883],{"class":55},">[]>(_.",[49,9885,9657],{"class":262},[49,9887,9888],{"class":55},"(globalProps.actions));\n",[49,9890,9891,9893,9896,9898,9900,9902,9904,9906,9908],{"class":51,"line":94},[49,9892,330],{"class":63},[49,9894,9895],{"class":333}," localSelectedItems",[49,9897,343],{"class":63},[49,9899,9825],{"class":262},[49,9901,56],{"class":55},[49,9903,6807],{"class":262},[49,9905,9832],{"class":55},[49,9907,9657],{"class":262},[49,9909,9910],{"class":55},"(globalProps.selectedItems));\n",[11,9912,7450,9913,9915,9916,9919,9920,9922],{},[18,9914,9225],{}," يلفّ المصفوفات في proxy تفاعلي يتتبّع كل تعديل صغير. في جدول من 1000 صف، يعني هذا 1000 proxy على الأقل، مع ما يتبعها من proxies متشعّبة داخل كل صف. معظم هذا الحمل ضائع. المكوّن نفسه يعيد إسناد ",[18,9917,9918],{},".value"," بأكمله كل مرة، ولا يعدّل أي صف داخليًا في مكانه. الـ ",[18,9921,9268],{}," يقول: تتبّع تغيّر المرجع فقط ولا تتعمّق. وهذا هو العقد المطلوب بالضبط.",[11,9924,9925,9926,9929,9930,9932],{},"الاستنساخ هو النصف الثاني من القصة. ",[18,9927,9928],{},"_.cloneDeep(globalProps.items)"," يأخذ لقطة من بيانات الأب لحظة الإسناد، ثم يعمل الجدول على نسخته الخاصة. هذا يهمّ في الفرز تحديدًا: حين يفرز المستخدم، يعدّل الجدول ",[18,9931,9797],{}," عنده دون أن يمسّ مصفوفة الأب. لو حذفنا الاستنساخ، لتحوّل الفرز البصري إلى تعديل مباشر على حالة الأب، وهنا يظهر أسوأ صنف من الأخطاء المعروفة بـ \"التصدير من الخادم لا يطابق ما كان على الشاشة\".",[11,9934,9935,9936,337],{},"المراقبة المتجدّدة تستعمل ",[18,9937,9791],{},[40,9939,9941],{"className":1893,"code":9940,"language":1895,"meta":45,"style":45},"debouncedWatch(\n  () => globalProps.items,\n  () => (localItems.value = _.cloneDeep(globalProps.items)),\n  { deep: true, debounce: 100 }\n);\n\ndebouncedWatch(\n  () => globalProps.selectedItems,\n  () => (localSelectedItems.value = _.cloneDeep(globalProps.selectedItems)),\n  { deep: true, immediate: true, debounce: 50 }\n);\n",[18,9942,9943,9949,9958,9976,9991,9995,9999,10005,10014,10032,10050],{"__ignoreMap":45},[49,9944,9945,9947],{"class":51,"line":52},[49,9946,9791],{"class":262},[49,9948,2114],{"class":55},[49,9950,9951,9953,9955],{"class":51,"line":82},[49,9952,3073],{"class":55},[49,9954,648],{"class":63},[49,9956,9957],{"class":55}," globalProps.items,\n",[49,9959,9960,9962,9964,9967,9969,9971,9973],{"class":51,"line":88},[49,9961,3073],{"class":55},[49,9963,648],{"class":63},[49,9965,9966],{"class":55}," (localItems.value ",[49,9968,67],{"class":63},[49,9970,9654],{"class":55},[49,9972,9657],{"class":262},[49,9974,9975],{"class":55},"(globalProps.items)),\n",[49,9977,9978,9981,9983,9986,9989],{"class":51,"line":94},[49,9979,9980],{"class":55},"  { deep: ",[49,9982,774],{"class":333},[49,9984,9985],{"class":55},", debounce: ",[49,9987,9988],{"class":333},"100",[49,9990,2247],{"class":55},[49,9992,9993],{"class":51,"line":363},[49,9994,630],{"class":55},[49,9996,9997],{"class":51,"line":374},[49,9998,325],{"emptyLinePlaceholder":324},[49,10000,10001,10003],{"class":51,"line":385},[49,10002,9791],{"class":262},[49,10004,2114],{"class":55},[49,10006,10007,10009,10011],{"class":51,"line":662},[49,10008,3073],{"class":55},[49,10010,648],{"class":63},[49,10012,10013],{"class":55}," globalProps.selectedItems,\n",[49,10015,10016,10018,10020,10023,10025,10027,10029],{"class":51,"line":691},[49,10017,3073],{"class":55},[49,10019,648],{"class":63},[49,10021,10022],{"class":55}," (localSelectedItems.value ",[49,10024,67],{"class":63},[49,10026,9654],{"class":55},[49,10028,9657],{"class":262},[49,10030,10031],{"class":55},"(globalProps.selectedItems)),\n",[49,10033,10034,10036,10038,10041,10043,10045,10048],{"class":51,"line":696},[49,10035,9980],{"class":55},[49,10037,774],{"class":333},[49,10039,10040],{"class":55},", immediate: ",[49,10042,774],{"class":333},[49,10044,9985],{"class":55},[49,10046,10047],{"class":333},"50",[49,10049,2247],{"class":55},[49,10051,10052],{"class":51,"line":723},[49,10053,630],{"class":55},[11,10055,10056],{},"لماذا debounce؟ لأن حالة الأب كثيرًا ما تتخبط. تغيير مرشِّح واحد على مستوى الأب قد يُحدث ثلاث إعادات render متتالية: تحديث الفورم، ثم تحديث سلسلة الاستعلام، ثم استجابة الجلب التي تُبدّل بيانات النائب. دون debounce، سيستنسخ الجدول عناصره ثلاث مرات في نفس الإطار. الـ 100ms للعناصر و50ms للتحديد أرقام استقرّيت عليها من مراقبة السلوك على الإنتاج، وليست مقدّسة. لو كانت لديّ الشهية لجعلتها props بحدّ ذاتها.",[11,10058,10059,10060,10063],{},"مراقب الأعمدة والإجراءات ",[7077,10061,10062],{},"ليس"," مُخفَّفًا بـ debounce:",[40,10065,10067],{"className":1893,"code":10066,"language":1895,"meta":45,"style":45},"localColumns.value = handleColumns();\nwatch(\n  () => [globalProps.columns, globalProps.actions],\n  () => {\n    localActions.value = _.cloneDeep(globalProps.actions);\n    localColumns.value = handleColumns();\n  },\n  { deep: true, immediate: true }\n);\n",[18,10068,10069,10081,10087,10096,10104,10118,10129,10133,10145],{"__ignoreMap":45},[49,10070,10071,10074,10076,10079],{"class":51,"line":52},[49,10072,10073],{"class":55},"localColumns.value ",[49,10075,67],{"class":63},[49,10077,10078],{"class":262}," handleColumns",[49,10080,3477],{"class":55},[49,10082,10083,10085],{"class":51,"line":82},[49,10084,2111],{"class":262},[49,10086,2114],{"class":55},[49,10088,10089,10091,10093],{"class":51,"line":88},[49,10090,3073],{"class":55},[49,10092,648],{"class":63},[49,10094,10095],{"class":55}," [globalProps.columns, globalProps.actions],\n",[49,10097,10098,10100,10102],{"class":51,"line":94},[49,10099,3073],{"class":55},[49,10101,648],{"class":63},[49,10103,1625],{"class":55},[49,10105,10106,10109,10111,10113,10115],{"class":51,"line":363},[49,10107,10108],{"class":55},"    localActions.value ",[49,10110,67],{"class":63},[49,10112,9654],{"class":55},[49,10114,9657],{"class":262},[49,10116,10117],{"class":55},"(globalProps.actions);\n",[49,10119,10120,10123,10125,10127],{"class":51,"line":374},[49,10121,10122],{"class":55},"    localColumns.value ",[49,10124,67],{"class":63},[49,10126,10078],{"class":262},[49,10128,3477],{"class":55},[49,10130,10131],{"class":51,"line":385},[49,10132,2237],{"class":55},[49,10134,10135,10137,10139,10141,10143],{"class":51,"line":662},[49,10136,9980],{"class":55},[49,10138,774],{"class":333},[49,10140,10040],{"class":55},[49,10142,774],{"class":333},[49,10144,2247],{"class":55},[49,10146,10147],{"class":51,"line":691},[49,10148,630],{"class":55},[11,10150,10151,10152,10155],{},"تغييرات الأعمدة والإجراءات نادرة. عادةً ما تُصرَّح مرة واحدة في أعلى ",[18,10153,10154],{},"\u003Cscript setup>"," ولا تُلمس بعدها أبدًا. الـ debounce سيُدخل تأخيرًا محسوسًا في الحالة النادرة التي تتغير فيها فعلًا، مثل admin يبدّل ظهور عمود. عدم التماثل (debounce على العناصر ولا debounce على الأعمدة) يعكس بدقة الطريقة التي يستخدم بها الناس المكوّن.",[26,10157,10159],{"id":10158},"التحميل-والفراغ-والترقيم-الزخارف-المحيطة","التحميل والفراغ والترقيم: الزخارف المحيطة",[11,10161,10162],{},"الزخارف المحيطة قصة صغيرة لا يكتبها أحد.",[11,10164,10165,10166,337],{},"حالة التحميل عبارة عن طبقة موضوعة بشكل مطلق مع ",[18,10167,10168],{},"pointer-events-none",[40,10170,10172],{"className":42,"code":10171,"language":44,"meta":45,"style":45},"\u003Cdiv\n  v-if=\"globalProps.loading\"\n  class=\"bg-white\u002F70 flex pointer-events-none items-center inset-0 justify-center absolute z-10 backdrop-blur-[1px]\"\n>\n  \u003CLoadingComp \u002F>\n\u003C\u002Fdiv>\n",[18,10173,10174,10180,10194,10203,10207,10212],{"__ignoreMap":45},[49,10175,10176,10178],{"class":51,"line":52},[49,10177,56],{"class":55},[49,10179,1068],{"class":59},[49,10181,10182,10185,10187,10189,10192],{"class":51,"line":82},[49,10183,10184],{"class":63},"  v-if",[49,10186,67],{"class":55},[49,10188,71],{"class":70},[49,10190,10191],{"class":55},"globalProps.loading",[49,10193,4195],{"class":70},[49,10195,10196,10198,10200],{"class":51,"line":88},[49,10197,1073],{"class":262},[49,10199,67],{"class":55},[49,10201,10202],{"class":70},"\"bg-white\u002F70 flex pointer-events-none items-center inset-0 justify-center absolute z-10 backdrop-blur-[1px]\"\n",[49,10204,10205],{"class":51,"line":94},[49,10206,79],{"class":55},[49,10208,10209],{"class":51,"line":363},[49,10210,10211],{"class":55},"  \u003CLoadingComp \u002F>\n",[49,10213,10214,10216,10218],{"class":51,"line":374},[49,10215,97],{"class":55},[49,10217,60],{"class":59},[49,10219,79],{"class":55},[11,10221,10222,10223,10225],{},"التفصيلة المهمّة هي ",[18,10224,10168],{},". بدونها، تعترض الطبقة النقرات على الصفوف تحتها. الطبقة شفافة بصريًا، ومع ذلك لن يستطيع المستخدم التفاعل مع الجدول المشوّش. النيّة هنا \"أظهِر أن شيئًا يجري، لا تحرم المستخدم من تحديد صف لا يزال يراه\". الطبقة مجرد إشارة، لا حاجز.",[11,10227,10228],{},"حالة الفراغ صف داخل الجدول نفسه، لا شقيقًا له:",[40,10230,10232],{"className":42,"code":10231,"language":44,"meta":45,"style":45},"\u003CATBody :columns=\"localColumns\" :items=\"localItems\">\n  \u003Ctr v-if=\"localItems.length === 0 && !globalProps.loading\">\n    \u003Ctd :colspan=\"localColumns.length\" class=\"p-4\">\n      \u003CNoData :title=\"globalProps.noDataTitle\" \u002F>\n    \u003C\u002Ftd>\n  \u003C\u002Ftr>\n\u003C\u002FATBody>\n",[18,10233,10234,10268,10273,10278,10283,10288,10292],{"__ignoreMap":45},[49,10235,10236,10238,10240,10242,10245,10247,10249,10251,10253,10255,10258,10260,10262,10264,10266],{"class":51,"line":52},[49,10237,56],{"class":55},[49,10239,7130],{"class":59},[49,10241,259],{"class":55},[49,10243,10244],{"class":262},"columns",[49,10246,67],{"class":55},[49,10248,71],{"class":70},[49,10250,9800],{"class":55},[49,10252,71],{"class":70},[49,10254,259],{"class":55},[49,10256,10257],{"class":262},"items",[49,10259,67],{"class":55},[49,10261,71],{"class":70},[49,10263,9797],{"class":55},[49,10265,71],{"class":70},[49,10267,79],{"class":55},[49,10269,10270],{"class":51,"line":82},[49,10271,10272],{"class":55},"  \u003Ctr v-if=\"localItems.length === 0 && !globalProps.loading\">\n",[49,10274,10275],{"class":51,"line":88},[49,10276,10277],{"class":55},"    \u003Ctd :colspan=\"localColumns.length\" class=\"p-4\">\n",[49,10279,10280],{"class":51,"line":94},[49,10281,10282],{"class":55},"      \u003CNoData :title=\"globalProps.noDataTitle\" \u002F>\n",[49,10284,10285],{"class":51,"line":363},[49,10286,10287],{"class":55},"    \u003C\u002Ftd>\n",[49,10289,10290],{"class":51,"line":374},[49,10291,7409],{"class":55},[49,10293,10294,10296,10298],{"class":51,"line":385},[49,10295,97],{"class":55},[49,10297,7130],{"class":59},[49,10299,79],{"class":55},[11,10301,10302,10303,10305,10306,10309,10310,10313],{},"تمريرها عبر slot الـ ",[18,10304,7142],{}," هو ما يجعلها ترث حدّ الجدول ومسافاته. لو وضعناها في ",[18,10307,10308],{},"\u003Cdiv>"," شقيق، لاحتجنا أنماطًا منفصلة لضبط محاذاتها، أما ",[18,10311,10312],{},"\u003Ctr colspan>"," فيحصل على التخطيط مجانًا.",[11,10315,10316,10317,337],{},"الترقيم اختياري ويعمل عبر prop اسمه ",[18,10318,10319],{},"pagination",[40,10321,10323],{"className":42,"code":10322,"language":44,"meta":45,"style":45},"\u003Cdiv v-if=\"globalProps.pagination && !globalProps.loading\">\n  \u003CAPagination\n    :model-value=\"globalProps.pagination.modelValue\"\n    :page-size=\"globalProps.pagination.pageSize\"\n    :total-items=\"globalProps.pagination.totalItems\"\n    @update:page-size=\"globalProps.pagination.onPageSizeChange\"\n    @update:model-value=\"globalProps.pagination.onUpdateCurrentPage\"\n  \u002F>\n\u003C\u002Fdiv>\n",[18,10324,10325,10350,10355,10360,10365,10370,10375,10380,10385],{"__ignoreMap":45},[49,10326,10327,10329,10331,10333,10335,10337,10340,10342,10344,10346,10348],{"class":51,"line":52},[49,10328,56],{"class":55},[49,10330,60],{"class":59},[49,10332,64],{"class":63},[49,10334,67],{"class":55},[49,10336,71],{"class":70},[49,10338,10339],{"class":55},"globalProps.pagination ",[49,10341,2513],{"class":63},[49,10343,9642],{"class":63},[49,10345,10191],{"class":55},[49,10347,71],{"class":70},[49,10349,79],{"class":55},[49,10351,10352],{"class":51,"line":82},[49,10353,10354],{"class":55},"  \u003CAPagination\n",[49,10356,10357],{"class":51,"line":88},[49,10358,10359],{"class":55},"    :model-value=\"globalProps.pagination.modelValue\"\n",[49,10361,10362],{"class":51,"line":94},[49,10363,10364],{"class":55},"    :page-size=\"globalProps.pagination.pageSize\"\n",[49,10366,10367],{"class":51,"line":363},[49,10368,10369],{"class":55},"    :total-items=\"globalProps.pagination.totalItems\"\n",[49,10371,10372],{"class":51,"line":374},[49,10373,10374],{"class":55},"    @update:page-size=\"globalProps.pagination.onPageSizeChange\"\n",[49,10376,10377],{"class":51,"line":385},[49,10378,10379],{"class":55},"    @update:model-value=\"globalProps.pagination.onUpdateCurrentPage\"\n",[49,10381,10382],{"class":51,"line":662},[49,10383,10384],{"class":55},"  \u002F>\n",[49,10386,10387,10389,10391],{"class":51,"line":691},[49,10388,97],{"class":55},[49,10390,60],{"class":59},[49,10392,79],{"class":55},[11,10394,10395,10396,10398,10399,394,10402,10405],{},"هناك تفصيلتان تستحقّان الإشارة. أولًا، مكوّن الترقيم مخفيّ أثناء التحميل. لو تركناه ظاهرًا، لبقيت قائمة حجم الصفحة نشطة أثناء الجلب، وقد تُطلق نقرة سريعة ثانية طلب صفحة إضافيًا. وثانيًا، تُمرَّر callbacks داخل كائن ",[18,10397,10319],{}," كحقول (",[18,10400,10401],{},"onPageSizeChange",[18,10403,10404],{},"onUpdateCurrentPage",") لا كـ props منفصلة. لستُ متأكدًا أن هذا كان القرار الصائب، وإصدار emit كان سيبدو أقرب إلى روح Vue. لكنه يحافظ على قصة \"الترقيم كتلة إعداد واحدة\" نظيفة.",[26,10407,10409],{"id":10408},"ما-كنتُ-سأغيّره-في-إعادة-كتابة-رابعة","ما كنتُ سأغيّره في إعادة كتابة رابعة",[11,10411,10412],{},"بضعة مواضع لا يمثّل كود اليوم فيها ما كنتُ سأكتبه لو بدأت من الصفر:",[8674,10414,10415,10427,10443,10459,10465],{},[3673,10416,10417,10426],{},[36,10418,10419,10420,394,10422,7454,10424,527],{},"ازدواج ",[18,10421,7876],{},[18,10423,7880],{},[18,10425,7133],{}," prop منفصلان يشيران إلى البيانات نفسها. دمجهما في prop واحد كان سيوفّر spread إضافيًا وفرعًا زائدًا. تركتُهما لأن مواقع الاستدعاء القديمة تعتمد على الاسم.",[3673,10428,10429,10434,10435,10438,10439,10442],{},[36,10430,10431,10433],{},[18,10432,8307],{}," في الخلايا الافتراضية."," الفخّ حقيقي، لكن الأرغونومية تستحقّ. لو أعدتُ الكتابة، فسأذهب إما إلى ",[18,10436,10437],{},"textContent"," افتراضيًا مع جعل الـ HTML خيارًا صريحًا عبر حقل في العمود (",[18,10440,10441],{},"unsafe: true",")، وإما إلى نهج مبني بالكامل على slots. كلاهما يحتاج إعادة كتابة، ولا أحدهما مستعجل.",[3673,10444,10445,10451,10452,10455,10456,10458],{},[36,10446,10447,10448,10450],{},"تحويل ",[18,10449,7330],{}," داخل الرأس."," يعمل، لكن جعل ",[18,10453,10454],{},"record?: S"," اختياريًا داخل ",[18,10457,7133],{}," سيكون أكثر أمانة. الرأس لا يمتلك سجلًا، فليقل النوع ذلك صراحة.",[3673,10460,10461,10464],{},[36,10462,10463],{},"قيم الـ debounce المكتوبة يدويًا (50ms و100ms)."," يجب أن تكون props. ثلاث دقائق عمل، ومع ذلك أؤجّلها كل مرة.",[3673,10466,10467,10476,10477,10479],{},[36,10468,7450,10469,10472,10473,10475],{},[18,10470,10471],{},"Cell"," المحسوب مع ",[18,10474,7901],{}," المفتاح فيه معلَّق على حالة الفرز."," ما زلتُ أُحدّق فيها. إما أن المفتاح غير ضروري (احذفه وانتظر ماذا يحصل)، وإما أنه يُخفي خطأ توقيت في ",[18,10478,7916],{}," يجب أن أستطيع إعادة إنتاجه. أنا مدين للكود بذلك التحقيق.",[11,10481,10482,10483,10485,10486,10488,10489,10491],{},"لا شيء من هذه هو السبب في أنني أنصح بهذا النمط. السبب أبسط وأملّ من هذا كلّه: سكيمة العمود كبيانات، وإيقاع ",[18,10484,9268],{}," مع الاستنساخ مع الـ debounce، والتحديد المفهرَس بـ Map، و",[18,10487,9079],{}," على مكوّن الصف. كل واحد منها من الأشياء التي تكتبها في المرة الثانية، بعد أن تكون النسخة الأولى قد عضّتك بالفعل. وهذه هي قصة ",[18,10490,6448],{}," كلها. الشكل الذي استقرّ عليه هو شكل الشيء الذي كففتُ عن إعادة كتابته.",[219,10493,10494],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":45,"searchDepth":82,"depth":82,"links":10496},[10497,10499,10501,10506,10508,10509,10511,10513,10514,10516,10517],{"id":6455,"depth":82,"text":10498},"من كلاس IColumn إلى interface مكتوب بأنواع",{"id":7123,"depth":82,"text":10500},"ثلاثي الـ render: ATHead وATBody وATCell",{"id":7477,"depth":82,"text":10502,"children":10503},"ATCell: مكوّن واحد بشكلَين، مبنيّ عبر h()",[10504],{"id":7887,"depth":88,"text":10505},"الـ :key=\"sortValue?.applied\" الذي يبدو خطأً مطبعيًا",{"id":7920,"depth":82,"text":10507},"handleValueBasedOnKey: مسارات بالنقاط داخل الصفوف",{"id":8321,"depth":82,"text":8322},{"id":8744,"depth":82,"text":10510},"التحديد على شكل Map\u003Cstring, T>",{"id":9075,"depth":82,"text":10512},"الإجراءات، وmarkRaw الذي أثبت جدواه",{"id":9341,"depth":82,"text":9342},{"id":9783,"depth":82,"text":10515},"التفاعلية: shallowRef وcloneDeep وdebouncedWatch",{"id":10158,"depth":82,"text":10159},{"id":10408,"depth":82,"text":10409},"2026-04-19","جولة كاملة في جدول بيانات Vue مُعمَّم بأنواع صارمة، بأعمدة مُعرَّفة كسكيمة، وتحديد صفوف قائم على Map، وأزرار إجراءات لكل صف عبر markRaw، ودورة فرز من ثلاث نقرات.",[10521,10522,9079,9268,10523,10524,10525,9791,10526,1841],"جدول بيانات Vue 3","مكوّن TypeScript مُعمَّم","تحديد الصفوف","فرز الأعمدة","تحليل مسارات بالنقاط","مكوّن واجهة",{},"\u002Fwrites-ar\u002Fanatomy-of-adatatable",{"title":6433,"description":10519},"writes-ar\u002Fanatomy-of-adatatable","7-nOCz0mwXbkHvNV5ZA2fsV31ZK1i0hzHSGTY2Z5KFI",{"id":10533,"title":10534,"body":10535,"date":12361,"description":12362,"description_ar":1836,"draft":1837,"extension":1838,"image":1836,"keywords":12363,"lang":1849,"meta":12369,"navigation":324,"path":12370,"seo":12371,"stem":12372,"title_ar":1836,"updatedAt":12361,"__hash__":12373},"writesAr\u002Fwrites-ar\u002Finside-asentinel-pagination.md","أربع مشكلات في أيّ قائمة infinite-scroll، وطريقة إصلاحها",{"type":8,"value":10536,"toc":12344},[10537,10540,10550,10560,10563,10567,10570,10660,10667,10674,10681,10688,10691,10847,10861,10864,10925,10928,11004,11011,11018,11021,11040,11047,11050,11121,11126,11392,11410,11414,11420,11455,11458,11586,11600,11604,11611,11614,11617,11759,11762,11773,11780,11787,11790,11805,11811,11815,11829,11832,11835,11875,11900,11907,11910,12033,12051,12061,12068,12071,12074,12292,12317,12328,12332,12338,12341],[11,10538,10539],{},"الـ infinite scroll من الميزات التي تبدو بسيطة وأنت ترسمها على السبورة، ثم تتحوّل إلى شيء غريب حين تصطدم بالاستخدام الحقيقي. المسار السعيد يتّسع لشريحة واحدة: تضع IntersectionObserver في أسفل القائمة، تُطلق fetch عند التقاطع، وتُلحق النتائج. اشحنها وانتهِ.",[11,10541,10542,10543,10546,10547,527],{},"بعدها تبدأ المشكلات في العثور عليك. يغيّر المستخدم فلتراً فيرجع لك استدعاء الصفحة التالية ببيانات الفلتر القديم. يخرج طلبَا \"load more\" في نفس الـ tick لأن المستخدم مرّر بسرعة. يُفرَّغ المكوّن بينما لا يزال observer نشِطاً، ويحتفظ closure الخاص بالـ callback بالصفحة كلها في مأمن من الـ GC. يعمل الـ handler عندك على dev ثم ينكسر على prod لأن أحدهم استدعى ",[18,10544,10545],{},"fetcher()"," بدلاً من ",[18,10548,10549],{},"fetcher(page)",[11,10551,10552,10555,10556,10559],{},[18,10553,10554],{},"ASentinelPagination"," هو جوابي عن هذا كله. بنيتُه في نحو 230 سطراً داخل ملف ",[18,10557,10558],{},".vue"," واحد، وليس فيه أي ذكاء استثنائي. ما يستحق الكتابة عنه ليس المعمار، بل قائمة المشكلات المحددة التي يتفاداها، والأسطر التي تتفاداها بالضبط. وهذا محور التدوينة.",[11,10561,10562],{},"سأبني كل شيء حول أربع مشكلات شحنتُ كل واحدة منها مرة على الأقل في محاولات سابقة.",[26,10564,10566],{"id":10565},"الـ-api-الذي-يكتبه-المستهلك","الـ API الذي يكتبه المستهلك",[11,10568,10569],{},"قبل أن أدخل في المشكلات، هذا ما يكتبه المستهلك فعلاً:",[40,10571,10573],{"className":42,"code":10572,"language":44,"meta":45,"style":45},"\u003CASentinelPagination :fetch-handler=\"loadProducts\">\n  \u003Ctemplate #card=\"{ item }\">\n    \u003CProductCard :product=\"item\" \u002F>\n  \u003C\u002Ftemplate>\n  \u003Ctemplate #initialLoading=\"{ count }\">\n    \u003CProductCardSkeleton v-for=\"i in count\" :key=\"i\" \u002F>\n  \u003C\u002Ftemplate>\n  \u003Ctemplate #loadingMore=\"{ count }\">\n    \u003CProductCardSkeleton v-for=\"i in count\" :key=\"i\" \u002F>\n  \u003C\u002Ftemplate>\n  \u003Ctemplate #emptyState>\n    \u003CNoResults \u002F>\n  \u003C\u002Ftemplate>\n\u003C\u002FASentinelPagination>\n",[18,10574,10575,10597,10602,10607,10611,10616,10621,10625,10630,10634,10638,10643,10648,10652],{"__ignoreMap":45},[49,10576,10577,10579,10581,10583,10586,10588,10590,10593,10595],{"class":51,"line":52},[49,10578,56],{"class":55},[49,10580,10554],{"class":59},[49,10582,259],{"class":55},[49,10584,10585],{"class":262},"fetch-handler",[49,10587,67],{"class":55},[49,10589,71],{"class":70},[49,10591,10592],{"class":55},"loadProducts",[49,10594,71],{"class":70},[49,10596,79],{"class":55},[49,10598,10599],{"class":51,"line":82},[49,10600,10601],{"class":55},"  \u003Ctemplate #card=\"{ item }\">\n",[49,10603,10604],{"class":51,"line":88},[49,10605,10606],{"class":55},"    \u003CProductCard :product=\"item\" \u002F>\n",[49,10608,10609],{"class":51,"line":94},[49,10610,5250],{"class":55},[49,10612,10613],{"class":51,"line":363},[49,10614,10615],{"class":55},"  \u003Ctemplate #initialLoading=\"{ count }\">\n",[49,10617,10618],{"class":51,"line":374},[49,10619,10620],{"class":55},"    \u003CProductCardSkeleton v-for=\"i in count\" :key=\"i\" \u002F>\n",[49,10622,10623],{"class":51,"line":385},[49,10624,5250],{"class":55},[49,10626,10627],{"class":51,"line":662},[49,10628,10629],{"class":55},"  \u003Ctemplate #loadingMore=\"{ count }\">\n",[49,10631,10632],{"class":51,"line":691},[49,10633,10620],{"class":55},[49,10635,10636],{"class":51,"line":696},[49,10637,5250],{"class":55},[49,10639,10640],{"class":51,"line":723},[49,10641,10642],{"class":55},"  \u003Ctemplate #emptyState>\n",[49,10644,10645],{"class":51,"line":744},[49,10646,10647],{"class":55},"    \u003CNoResults \u002F>\n",[49,10649,10650],{"class":51,"line":768},[49,10651,5250],{"class":55},[49,10653,10654,10656,10658],{"class":51,"line":779},[49,10655,97],{"class":55},[49,10657,10554],{"class":59},[49,10659,79],{"class":55},[11,10661,10662,10663,10666],{},"أربع slots، وprop واحد يهم فعلاً. الـ handler يرجع ",[18,10664,10665],{},"{ items: T[], pagination?: { current_page, per_page, total, last_page } }",". الـ pagination اختياري، فإن لم يرجعه الـ backend تُحمَّل القائمة مرة واحدة وتتوقف. سلوك متواضع، لكنه صادق.",[26,10668,10670,10673],{"id":10669},"fetchhandlerlength-توزيع-بحسب-الـ-arity",[18,10671,10672],{},"fetchHandler.length",": توزيع بحسب الـ arity",[11,10675,10676,10677,10680],{},"بعض واجهات الـ API يحتاج رقم صفحة، وبعضها لا يحتاجه. بعض الـ endpoints يعمل pagination على الـ server، وبعضها يرجع كل شيء دفعة واحدة. لو فرضتُ على المكوّن شكلاً واحداً، لاضطرّ المستهلكون إلى كتابة closures وسيطة تُجبر الـ fetcher على التوقيع المتوقع. ولو فرضتُ الشكل الآخر، لكتب من لا يهتم بالـ pagination شيئاً مثل ",[18,10678,10679],{},"(_page) => ..."," في كل مكان مع تجاهل الوسيط.",[11,10682,10683,10684,10687],{},"الحل الخاطئ هو إضافة prop باسم ",[18,10685,10686],{},"mode: 'paged' | 'stateless'",". هذا إعداد لشيء يعبّر عنه توقيع الدالة نفسها.",[11,10689,10690],{},"الحل الصحيح، كما تبيّن، هو فحص الدالة في وقت التشغيل:",[40,10692,10694],{"className":1893,"code":10693,"language":1895,"meta":45,"style":45},"async function runFetchHandler(): Promise\u003CISPPaginationHandlerResult\u003CT>> {\n  if (props.fetchHandler.length >= 1) {\n    const fn = props.fetchHandler as (\n      p: ISPPaginationMeta\n    ) => Promise\u003CISPPaginationHandlerResult\u003CT>>;\n    return await fn(pagination.value);\n  }\n\n  const fn = props.fetchHandler as () => Promise\u003CISPPaginationHandlerResult\u003CT>>;\n  return await fn();\n}\n",[18,10695,10696,10724,10739,10756,10766,10786,10797,10801,10805,10833,10843],{"__ignoreMap":45},[49,10697,10698,10700,10702,10705,10708,10710,10712,10714,10717,10719,10721],{"class":51,"line":52},[49,10699,3752],{"class":63},[49,10701,3347],{"class":63},[49,10703,10704],{"class":262}," runFetchHandler",[49,10706,10707],{"class":55},"()",[49,10709,337],{"class":63},[49,10711,3370],{"class":262},[49,10713,56],{"class":55},[49,10715,10716],{"class":262},"ISPPaginationHandlerResult",[49,10718,56],{"class":55},[49,10720,6807],{"class":262},[49,10722,10723],{"class":55},">> {\n",[49,10725,10726,10728,10731,10733,10735,10737],{"class":51,"line":82},[49,10727,2449],{"class":63},[49,10729,10730],{"class":55}," (props.fetchHandler.",[49,10732,2626],{"class":333},[49,10734,2629],{"class":63},[49,10736,2811],{"class":333},[49,10738,2149],{"class":55},[49,10740,10741,10743,10746,10748,10751,10753],{"class":51,"line":88},[49,10742,2204],{"class":63},[49,10744,10745],{"class":333}," fn",[49,10747,343],{"class":63},[49,10749,10750],{"class":55}," props.fetchHandler ",[49,10752,2657],{"class":63},[49,10754,10755],{"class":55}," (\n",[49,10757,10758,10761,10763],{"class":51,"line":94},[49,10759,10760],{"class":601},"      p",[49,10762,337],{"class":63},[49,10764,10765],{"class":262}," ISPPaginationMeta\n",[49,10767,10768,10771,10773,10775,10777,10779,10781,10783],{"class":51,"line":363},[49,10769,10770],{"class":55},"    ) ",[49,10772,648],{"class":63},[49,10774,3370],{"class":262},[49,10776,56],{"class":55},[49,10778,10716],{"class":262},[49,10780,56],{"class":55},[49,10782,6807],{"class":262},[49,10784,10785],{"class":55},">>;\n",[49,10787,10788,10790,10792,10794],{"class":51,"line":374},[49,10789,2945],{"class":63},[49,10791,3555],{"class":63},[49,10793,10745],{"class":262},[49,10795,10796],{"class":55},"(pagination.value);\n",[49,10798,10799],{"class":51,"line":385},[49,10800,2589],{"class":55},[49,10802,10803],{"class":51,"line":662},[49,10804,325],{"emptyLinePlaceholder":324},[49,10806,10807,10809,10811,10813,10815,10817,10819,10821,10823,10825,10827,10829,10831],{"class":51,"line":691},[49,10808,2603],{"class":63},[49,10810,10745],{"class":333},[49,10812,343],{"class":63},[49,10814,10750],{"class":55},[49,10816,2657],{"class":63},[49,10818,4441],{"class":55},[49,10820,648],{"class":63},[49,10822,3370],{"class":262},[49,10824,56],{"class":55},[49,10826,10716],{"class":262},[49,10828,56],{"class":55},[49,10830,6807],{"class":262},[49,10832,10785],{"class":55},[49,10834,10835,10837,10839,10841],{"class":51,"line":696},[49,10836,2962],{"class":63},[49,10838,3555],{"class":63},[49,10840,10745],{"class":262},[49,10842,3477],{"class":55},[49,10844,10845],{"class":51,"line":723},[49,10846,1719],{"class":55},[11,10848,7450,10849,10852,10853,10856,10857,10860],{},[18,10850,10851],{},"Function.prototype.length"," يرجع عدد الوسائط الشكلية التي تُصرِّح بها الدالة. لو كتب المستهلك ",[18,10854,10855],{},"loadProducts(p)"," فالقيمة 1، ولو كتب ",[18,10858,10859],{},"loadProducts()"," فالقيمة 0. المكوّن يتفرّع بناءً على ذلك.",[11,10862,10863],{},"من ناحية المستهلك، كلا الشكلين مقبول:",[40,10865,10867],{"className":1893,"code":10866,"language":1895,"meta":45,"style":45},"const loadAllProducts = () => $api('\u002Fproducts');                                   \u002F\u002F stateless\nconst loadProductsPage = (p) => $api('\u002Fproducts', { query: { page: p.current_page } }); \u002F\u002F paged\n",[18,10868,10869,10896],{"__ignoreMap":45},[49,10870,10871,10873,10876,10878,10880,10882,10885,10887,10890,10893],{"class":51,"line":52},[49,10872,330],{"class":63},[49,10874,10875],{"class":262}," loadAllProducts",[49,10877,343],{"class":63},[49,10879,4441],{"class":55},[49,10881,648],{"class":63},[49,10883,10884],{"class":262}," $api",[49,10886,625],{"class":55},[49,10888,10889],{"class":70},"'\u002Fproducts'",[49,10891,10892],{"class":55},");                                   ",[49,10894,10895],{"class":2472},"\u002F\u002F stateless\n",[49,10897,10898,10900,10903,10905,10907,10909,10911,10913,10915,10917,10919,10922],{"class":51,"line":82},[49,10899,330],{"class":63},[49,10901,10902],{"class":262}," loadProductsPage",[49,10904,343],{"class":63},[49,10906,2183],{"class":55},[49,10908,11],{"class":601},[49,10910,807],{"class":55},[49,10912,648],{"class":63},[49,10914,10884],{"class":262},[49,10916,625],{"class":55},[49,10918,10889],{"class":70},[49,10920,10921],{"class":55},", { query: { page: p.current_page } }); ",[49,10923,10924],{"class":2472},"\u002F\u002F paged\n",[11,10926,10927],{},"الاتحاد في TypeScript على نوع الـ prop يُبقي التوقيعَين متسقَين:",[40,10929,10931],{"className":1893,"code":10930,"language":1895,"meta":45,"style":45},"export type ISPFetchHandler\u003CT> =\n  | ((pagination: ISPPaginationMeta) => Promise\u003CISPPaginationHandlerResult\u003CT>>)\n  | (() => Promise\u003CISPPaginationHandlerResult\u003CT>>);\n",[18,10932,10933,10951,10982],{"__ignoreMap":45},[49,10934,10935,10937,10939,10942,10944,10946,10948],{"class":51,"line":52},[49,10936,3341],{"class":63},[49,10938,9357],{"class":63},[49,10940,10941],{"class":262}," ISPFetchHandler",[49,10943,56],{"class":55},[49,10945,6807],{"class":262},[49,10947,5553],{"class":55},[49,10949,10950],{"class":63},"=\n",[49,10952,10953,10956,10958,10960,10962,10965,10967,10969,10971,10973,10975,10977,10979],{"class":51,"line":82},[49,10954,10955],{"class":63},"  |",[49,10957,6577],{"class":55},[49,10959,10319],{"class":601},[49,10961,337],{"class":63},[49,10963,10964],{"class":262}," ISPPaginationMeta",[49,10966,807],{"class":55},[49,10968,648],{"class":63},[49,10970,3370],{"class":262},[49,10972,56],{"class":55},[49,10974,10716],{"class":262},[49,10976,56],{"class":55},[49,10978,6807],{"class":262},[49,10980,10981],{"class":55},">>)\n",[49,10983,10984,10986,10989,10991,10993,10995,10997,10999,11001],{"class":51,"line":88},[49,10985,10955],{"class":63},[49,10987,10988],{"class":55}," (() ",[49,10990,648],{"class":63},[49,10992,3370],{"class":262},[49,10994,56],{"class":55},[49,10996,10716],{"class":262},[49,10998,56],{"class":55},[49,11000,6807],{"class":262},[49,11002,11003],{"class":55},">>);\n",[11,11005,11006,11007,11010],{},"يبقى فخ واحد جدير بالذكر. الدوال السهمية التي تُصرِّح بوسيط دون أن تستعمله تُحسب أيضاً على أنها arity 1. لو كتب المستهلك ",[18,11008,11009],{},"(_p) => $api('\u002Fproducts')"," فسيقع على فرع الـ paged. لا مشكلة عملية في هذا لأن الـ handler يتجاهل الوسيط، لكنه من النوع الذي يُدوَّن مرة واحدة كي يجد الشخص التالي إجابة جاهزة لسؤال \"لماذا يُمرَّر pagination أصلاً؟\"",[26,11012,11014,11017],{"id":11013},"inflight-بوابة-واحدة-وثلاث-قراءات-مشتقة",[18,11015,11016],{},"inFlight",": بوابة واحدة، وثلاث قراءات مشتقة",[11,11019,11020],{},"هذه المشكلة كلاسيكية. يصل المستخدم إلى الأسفل، فينطلق observer ويبدأ fetch. يمرّر بكسلاً واحداً، فينطلق observer مرة أخرى لأن الـ sentinel لا يزال ظاهراً، ويبدأ fetch ثانٍ قبل أن يُحلّ الأول. بحسب التوقيت تحصل على:",[8674,11022,11023,11026,11037],{},[3673,11024,11025],{},"كلا الطلبين يرجعان الصفحة 2، فتتكرّر العناصر.",[3673,11027,11028,11029,11032,11033,11036],{},"طلب يخرج بـ ",[18,11030,11031],{},"current_page = 2"," وآخر بـ ",[18,11034,11035],{},"current_page = 3","، فتتشابك العناصر أو تُتخطّى الصفحة 2 بصمت.",[3673,11038,11039],{},"كلاهما يرجع الصفحة نفسها، فينكسر منطق de-dup عند المستهلك، ويقفز الـ layout.",[11,11041,11042,11043,11046],{},"الحل الساذج هو boolean واحد باسم ",[18,11044,11045],{},"isLoading",". لكنه لا يكفي، لأن لدينا نوعين مختلفين منطقياً من التحميل: الأولي بـ skeleton يملأ الشاشة، والصفحة التالية بـ skeleton صغير في الأسفل. أي boolean-ين قد يختلفان، وحين يختلفان يبدأ منطق البوابة في السؤال \"هل هذا النوع تحديداً جارٍ الآن؟\"، وهو سؤال خاطئ من الأساس.",[11,11048,11049],{},"السؤال الصحيح هو: \"هل يوجد أي شيء جارٍ الآن؟\" مصدر واحد للحقيقة، وقراءتان مشتقتان منه:",[40,11051,11053],{"className":1893,"code":11052,"language":1895,"meta":45,"style":45},"const initialLoading = ref(true);\nconst isFetchingMore = ref(false);\nconst inFlight = ref\u003C'init' | 'next' | null>(null);\n",[18,11054,11055,11072,11090],{"__ignoreMap":45},[49,11056,11057,11059,11062,11064,11066,11068,11070],{"class":51,"line":52},[49,11058,330],{"class":63},[49,11060,11061],{"class":333}," initialLoading",[49,11063,343],{"class":63},[49,11065,622],{"class":262},[49,11067,625],{"class":55},[49,11069,774],{"class":333},[49,11071,630],{"class":55},[49,11073,11074,11076,11079,11081,11083,11085,11088],{"class":51,"line":82},[49,11075,330],{"class":63},[49,11077,11078],{"class":333}," isFetchingMore",[49,11080,343],{"class":63},[49,11082,622],{"class":262},[49,11084,625],{"class":55},[49,11086,11087],{"class":333},"false",[49,11089,630],{"class":55},[49,11091,11092,11094,11097,11099,11101,11103,11106,11108,11111,11113,11115,11117,11119],{"class":51,"line":88},[49,11093,330],{"class":63},[49,11095,11096],{"class":333}," inFlight",[49,11098,343],{"class":63},[49,11100,622],{"class":262},[49,11102,56],{"class":55},[49,11104,11105],{"class":70},"'init'",[49,11107,679],{"class":63},[49,11109,11110],{"class":70}," 'next'",[49,11112,679],{"class":63},[49,11114,657],{"class":333},[49,11116,684],{"class":55},[49,11118,172],{"class":333},[49,11120,630],{"class":55},[11,11122,7450,11123,11125],{},[18,11124,11016],{}," هو البوابة. أما الاثنان الآخران فيتبدّل الـ template عليهما ليحدّد أي skeleton يعرضه. وتُضبط الثلاثة معاً:",[40,11127,11129],{"className":1893,"code":11128,"language":1895,"meta":45,"style":45},"const fetchData = async (mode: 'init' | 'next') => {\n  if (inFlight.value !== null) return;\n  if (mode === 'next' && (!hasNextPage.value || isFetchingMore.value)) return;\n\n  inFlight.value = mode;\n\n  try {\n    if (mode === 'init') initialLoading.value = true;\n    if (mode === 'next') isFetchingMore.value = true;\n\n    const result = await runFetchHandler();\n    applyPaginationFromHandlerResult(result);\n    items.value = mode === 'init' ? result.items : [...items.value, ...result.items];\n  } finally {\n    if (mode === 'init') initialLoading.value = false;\n    if (mode === 'next') isFetchingMore.value = false;\n    inFlight.value = null;\n  }\n};\n",[18,11130,11131,11162,11179,11208,11212,11222,11226,11232,11251,11270,11274,11288,11296,11329,11337,11355,11373,11384,11388],{"__ignoreMap":45},[49,11132,11133,11135,11138,11140,11142,11144,11147,11149,11152,11154,11156,11158,11160],{"class":51,"line":52},[49,11134,330],{"class":63},[49,11136,11137],{"class":262}," fetchData",[49,11139,343],{"class":63},[49,11141,3344],{"class":63},[49,11143,2183],{"class":55},[49,11145,11146],{"class":601},"mode",[49,11148,337],{"class":63},[49,11150,11151],{"class":70}," 'init'",[49,11153,679],{"class":63},[49,11155,11110],{"class":70},[49,11157,807],{"class":55},[49,11159,648],{"class":63},[49,11161,1625],{"class":55},[49,11163,11164,11166,11169,11171,11173,11175,11177],{"class":51,"line":82},[49,11165,2449],{"class":63},[49,11167,11168],{"class":55}," (inFlight.value ",[49,11170,3815],{"class":63},[49,11172,657],{"class":333},[49,11174,807],{"class":55},[49,11176,2197],{"class":63},[49,11178,319],{"class":55},[49,11180,11181,11183,11186,11188,11190,11192,11194,11196,11199,11201,11204,11206],{"class":51,"line":88},[49,11182,2449],{"class":63},[49,11184,11185],{"class":55}," (mode ",[49,11187,654],{"class":63},[49,11189,11110],{"class":70},[49,11191,8933],{"class":63},[49,11193,2183],{"class":55},[49,11195,2454],{"class":63},[49,11197,11198],{"class":55},"hasNextPage.value ",[49,11200,2693],{"class":63},[49,11202,11203],{"class":55}," isFetchingMore.value)) ",[49,11205,2197],{"class":63},[49,11207,319],{"class":55},[49,11209,11210],{"class":51,"line":94},[49,11211,325],{"emptyLinePlaceholder":324},[49,11213,11214,11217,11219],{"class":51,"line":363},[49,11215,11216],{"class":55},"  inFlight.value ",[49,11218,67],{"class":63},[49,11220,11221],{"class":55}," mode;\n",[49,11223,11224],{"class":51,"line":374},[49,11225,325],{"emptyLinePlaceholder":324},[49,11227,11228,11230],{"class":51,"line":385},[49,11229,2478],{"class":63},[49,11231,1625],{"class":55},[49,11233,11234,11236,11238,11240,11242,11245,11247,11249],{"class":51,"line":662},[49,11235,2138],{"class":63},[49,11237,11185],{"class":55},[49,11239,654],{"class":63},[49,11241,11151],{"class":70},[49,11243,11244],{"class":55},") initialLoading.value ",[49,11246,67],{"class":63},[49,11248,3174],{"class":333},[49,11250,319],{"class":55},[49,11252,11253,11255,11257,11259,11261,11264,11266,11268],{"class":51,"line":691},[49,11254,2138],{"class":63},[49,11256,11185],{"class":55},[49,11258,654],{"class":63},[49,11260,11110],{"class":70},[49,11262,11263],{"class":55},") isFetchingMore.value ",[49,11265,67],{"class":63},[49,11267,3174],{"class":333},[49,11269,319],{"class":55},[49,11271,11272],{"class":51,"line":696},[49,11273,325],{"emptyLinePlaceholder":324},[49,11275,11276,11278,11280,11282,11284,11286],{"class":51,"line":723},[49,11277,2204],{"class":63},[49,11279,9722],{"class":333},[49,11281,343],{"class":63},[49,11283,3555],{"class":63},[49,11285,10704],{"class":262},[49,11287,3477],{"class":55},[49,11289,11290,11293],{"class":51,"line":744},[49,11291,11292],{"class":262},"    applyPaginationFromHandlerResult",[49,11294,11295],{"class":55},"(result);\n",[49,11297,11298,11301,11303,11306,11308,11310,11312,11315,11317,11319,11321,11324,11326],{"class":51,"line":768},[49,11299,11300],{"class":55},"    items.value ",[49,11302,67],{"class":63},[49,11304,11305],{"class":55}," mode ",[49,11307,654],{"class":63},[49,11309,11151],{"class":70},[49,11311,6723],{"class":63},[49,11313,11314],{"class":55}," result.items ",[49,11316,337],{"class":63},[49,11318,7654],{"class":55},[49,11320,9696],{"class":63},[49,11322,11323],{"class":55},"items.value, ",[49,11325,9696],{"class":63},[49,11327,11328],{"class":55},"result.items];\n",[49,11330,11331,11333,11335],{"class":51,"line":779},[49,11332,2574],{"class":55},[49,11334,4053],{"class":63},[49,11336,1625],{"class":55},[49,11338,11339,11341,11343,11345,11347,11349,11351,11353],{"class":51,"line":784},[49,11340,2138],{"class":63},[49,11342,11185],{"class":55},[49,11344,654],{"class":63},[49,11346,11151],{"class":70},[49,11348,11244],{"class":55},[49,11350,67],{"class":63},[49,11352,3299],{"class":333},[49,11354,319],{"class":55},[49,11356,11357,11359,11361,11363,11365,11367,11369,11371],{"class":51,"line":789},[49,11358,2138],{"class":63},[49,11360,11185],{"class":55},[49,11362,654],{"class":63},[49,11364,11110],{"class":70},[49,11366,11263],{"class":55},[49,11368,67],{"class":63},[49,11370,3299],{"class":333},[49,11372,319],{"class":55},[49,11374,11375,11378,11380,11382],{"class":51,"line":820},[49,11376,11377],{"class":55},"    inFlight.value ",[49,11379,67],{"class":63},[49,11381,657],{"class":333},[49,11383,319],{"class":55},[49,11385,11386],{"class":51,"line":829},[49,11387,2589],{"class":55},[49,11389,11390],{"class":51,"line":834},[49,11391,5759],{"class":55},[11,11393,11394,11395,11398,11399,11402,11403,11405,11406,11409],{},"قاعدتان صغيرتان تُبقيان هذا مستقيماً. الـ early return يفحص ",[18,11396,11397],{},"inFlight.value !== null"," لا أي boolean بعينه، فإن كان init جارياً وأُطلق trigger للصفحة التالية، يُرفض الـ fetch الثاني حتى لو كان ",[18,11400,11401],{},"isFetchingMore"," لا يزال false. والإفراج عن البوابة يقع في ",[18,11404,4053],{}," لا في ",[18,11407,11408],{},"try","، فحين يرمي fetch استثناءً تُحرَّر البوابة أيضاً، ولا يُقفل المكوّن نفسه بصمت في وضع \"أرفض تحميل أي شيء\" بعد أول تعثّر شبكي.",[1013,11411,11413],{"id":11412},"الـ-sentinel-وعتبة-الـ-06","الـ sentinel، وعتبة الـ 0.6",[11,11415,11416,11417,337],{},"الـ sentinel نفسه عبارة عن div بارتفاع 1px مع ",[18,11418,11419],{},"aria-hidden",[40,11421,11423],{"className":42,"code":11422,"language":44,"meta":45,"style":45},"\u003Cdiv ref=\"sentinelRef\" class=\"h-1 min-w-[1px] w-full\" aria-hidden=\"true\" \u002F>\n",[18,11424,11425],{"__ignoreMap":45},[49,11426,11427,11429,11431,11433,11435,11438,11440,11442,11445,11448,11450,11453],{"class":51,"line":52},[49,11428,56],{"class":55},[49,11430,60],{"class":59},[49,11432,622],{"class":262},[49,11434,67],{"class":55},[49,11436,11437],{"class":70},"\"sentinelRef\"",[49,11439,999],{"class":262},[49,11441,67],{"class":55},[49,11443,11444],{"class":70},"\"h-1 min-w-[1px] w-full\"",[49,11446,11447],{"class":262}," aria-hidden",[49,11449,67],{"class":55},[49,11451,11452],{"class":70},"\"true\"",[49,11454,885],{"class":55},[11,11456,11457],{},"وفي الـ observer ضمانتان أحبّ أن أعزلهما:",[40,11459,11461],{"className":1893,"code":11460,"language":1895,"meta":45,"style":45},"observer = new IntersectionObserver(\n  (entries) => {\n    const entry = entries[0];\n    if (!entry?.isIntersecting) return;\n    if (!initialLoading.value && !isFetchingMore.value && hasNextPage.value) {\n      void fetchNextPage();\n    }\n  },\n  { root: null, rootMargin: '0px', threshold: 0.6 }\n);\n",[18,11462,11463,11477,11489,11505,11520,11543,11553,11557,11561,11582],{"__ignoreMap":45},[49,11464,11465,11468,11470,11472,11475],{"class":51,"line":52},[49,11466,11467],{"class":55},"observer ",[49,11469,67],{"class":63},[49,11471,3838],{"class":63},[49,11473,11474],{"class":262}," IntersectionObserver",[49,11476,2114],{"class":55},[49,11478,11479,11481,11483,11485,11487],{"class":51,"line":82},[49,11480,2124],{"class":55},[49,11482,6078],{"class":601},[49,11484,807],{"class":55},[49,11486,648],{"class":63},[49,11488,1625],{"class":55},[49,11490,11491,11493,11496,11498,11501,11503],{"class":51,"line":88},[49,11492,2204],{"class":63},[49,11494,11495],{"class":333}," entry",[49,11497,343],{"class":63},[49,11499,11500],{"class":55}," entries[",[49,11502,2839],{"class":333},[49,11504,5961],{"class":55},[49,11506,11507,11509,11511,11513,11516,11518],{"class":51,"line":94},[49,11508,2138],{"class":63},[49,11510,2183],{"class":55},[49,11512,2454],{"class":63},[49,11514,11515],{"class":55},"entry?.isIntersecting) ",[49,11517,2197],{"class":63},[49,11519,319],{"class":55},[49,11521,11522,11524,11526,11528,11531,11533,11535,11538,11540],{"class":51,"line":363},[49,11523,2138],{"class":63},[49,11525,2183],{"class":55},[49,11527,2454],{"class":63},[49,11529,11530],{"class":55},"initialLoading.value ",[49,11532,2513],{"class":63},[49,11534,9642],{"class":63},[49,11536,11537],{"class":55},"isFetchingMore.value ",[49,11539,2513],{"class":63},[49,11541,11542],{"class":55}," hasNextPage.value) {\n",[49,11544,11545,11548,11551],{"class":51,"line":374},[49,11546,11547],{"class":63},"      void",[49,11549,11550],{"class":262}," fetchNextPage",[49,11552,3477],{"class":55},[49,11554,11555],{"class":51,"line":385},[49,11556,2176],{"class":55},[49,11558,11559],{"class":51,"line":662},[49,11560,2237],{"class":55},[49,11562,11563,11566,11568,11571,11574,11577,11580],{"class":51,"line":691},[49,11564,11565],{"class":55},"  { root: ",[49,11567,172],{"class":333},[49,11569,11570],{"class":55},", rootMargin: ",[49,11572,11573],{"class":70},"'0px'",[49,11575,11576],{"class":55},", threshold: ",[49,11578,11579],{"class":333},"0.6",[49,11581,2247],{"class":55},[49,11583,11584],{"class":51,"line":696},[49,11585,630],{"class":55},[11,11587,11588,8714,11591,11593,11594,11597,11598,527],{},[18,11589,11590],{},"threshold: 0.6",[18,11592,2839],{},". الـ sentinel بارتفاع 1px، وعند ",[18,11595,11596],{},"threshold: 0"," يكفي أن يهتز بكسلاً واحداً داخل الـ viewport وخارجه أثناء التمرير حتى يُطلق الـ callback عدة مرات. عند 0.6 يصبح التقاطع أكثر معنى، فـ 60% من هدف بحجم 1px ليست كبيرة لكنها مستقرة أمام هذا الاهتزاز. وفحص الحالة داخل الـ callback يعيد تأكيد البوابة، لأنه بين لحظة إطلاق التقاطع ولحظة تشغيل الـ callback ربما بدأ تحميل آخر. الحارس ثلاثي الشروط هو خط الدفاع الثاني الـ idempotent خلف ",[18,11599,11016],{},[26,11601,11603],{"id":11602},"إعادة-الضبط-عند-تغيّر-هوية-الـ-fetcher","إعادة الضبط عند تغيّر هوية الـ fetcher",[11,11605,11606,11607,11610],{},"يضغط المستخدم على \"Category: Shoes\". يمرّر المكوّن الأب ",[18,11608,11609],{},"fetchHandler"," جديداً يحمل الفلتر الجديد. ما ينبغي أن يحدث: مسح القائمة، جلب الصفحة الأولى بالـ handler الجديد، وتركيب observer جديد.",[11,11612,11613],{},"أما إن لم تتعامل مع الموقف، فتبقى القائمة القديمة على الشاشة، وتُلحَق الصفحة الثانية للفلتر الجديد بالصفحة الأولى للفلتر القديم. يرى المستخدم مزيجاً من الأحذية وعناصر لا علاقة لها، وتخرج طلبات \"الصفحة 3\" مقابل handler بينما تُفسَّر مقابل حالة pagination تخص handler آخر.",[11,11615,11616],{},"المُشغِّل هو watch على الـ handler نفسه:",[40,11618,11620],{"className":1893,"code":11619,"language":1895,"meta":45,"style":45},"watch(\n  () => props.fetchHandler,\n  async () => {\n    observer?.disconnect();\n\n    items.value = [];\n    hasNextPage.value = false;\n    pagination.value = {\n      current_page: 1,\n      per_page: pagination.value.per_page,\n      total: 0,\n      last_page: 1,\n    };\n\n    await fetchData('init');\n    setupObserver();\n  }\n);\n",[18,11621,11622,11628,11637,11648,11658,11662,11671,11682,11691,11700,11705,11714,11723,11727,11731,11744,11751,11755],{"__ignoreMap":45},[49,11623,11624,11626],{"class":51,"line":52},[49,11625,2111],{"class":262},[49,11627,2114],{"class":55},[49,11629,11630,11632,11634],{"class":51,"line":82},[49,11631,3073],{"class":55},[49,11633,648],{"class":63},[49,11635,11636],{"class":55}," props.fetchHandler,\n",[49,11638,11639,11642,11644,11646],{"class":51,"line":88},[49,11640,11641],{"class":63},"  async",[49,11643,4441],{"class":55},[49,11645,648],{"class":63},[49,11647,1625],{"class":55},[49,11649,11650,11653,11656],{"class":51,"line":94},[49,11651,11652],{"class":55},"    observer?.",[49,11654,11655],{"class":262},"disconnect",[49,11657,3477],{"class":55},[49,11659,11660],{"class":51,"line":363},[49,11661,325],{"emptyLinePlaceholder":324},[49,11663,11664,11666,11668],{"class":51,"line":374},[49,11665,11300],{"class":55},[49,11667,67],{"class":63},[49,11669,11670],{"class":55}," [];\n",[49,11672,11673,11676,11678,11680],{"class":51,"line":385},[49,11674,11675],{"class":55},"    hasNextPage.value ",[49,11677,67],{"class":63},[49,11679,3299],{"class":333},[49,11681,319],{"class":55},[49,11683,11684,11687,11689],{"class":51,"line":662},[49,11685,11686],{"class":55},"    pagination.value ",[49,11688,67],{"class":63},[49,11690,1625],{"class":55},[49,11692,11693,11696,11698],{"class":51,"line":691},[49,11694,11695],{"class":55},"      current_page: ",[49,11697,2090],{"class":333},[49,11699,360],{"class":55},[49,11701,11702],{"class":51,"line":696},[49,11703,11704],{"class":55},"      per_page: pagination.value.per_page,\n",[49,11706,11707,11710,11712],{"class":51,"line":723},[49,11708,11709],{"class":55},"      total: ",[49,11711,2839],{"class":333},[49,11713,360],{"class":55},[49,11715,11716,11719,11721],{"class":51,"line":744},[49,11717,11718],{"class":55},"      last_page: ",[49,11720,2090],{"class":333},[49,11722,360],{"class":55},[49,11724,11725],{"class":51,"line":768},[49,11726,6766],{"class":55},[49,11728,11729],{"class":51,"line":779},[49,11730,325],{"emptyLinePlaceholder":324},[49,11732,11733,11736,11738,11740,11742],{"class":51,"line":784},[49,11734,11735],{"class":63},"    await",[49,11737,11137],{"class":262},[49,11739,625],{"class":55},[49,11741,11105],{"class":70},[49,11743,630],{"class":55},[49,11745,11746,11749],{"class":51,"line":789},[49,11747,11748],{"class":262},"    setupObserver",[49,11750,3477],{"class":55},[49,11752,11753],{"class":51,"line":820},[49,11754,2589],{"class":55},[49,11756,11757],{"class":51,"line":829},[49,11758,630],{"class":55},[11,11760,11761],{},"تحدث أربعة أشياء بالترتيب، وكل واحدة منها تحمل وزناً حقيقياً.",[11,11763,11764,11765,11768,11769,11772],{},"الفصل أولاً. لو لم أفصل الـ observer القديم لظلّ حياً أثناء إعادة الضبط، والـ sentinel لا يزال يتقاطع مع الـ viewport لأنه لا شيء تمرّر. حينها يستطيع الـ observer القديم إطلاق ",[18,11766,11767],{},"fetchNextPage()"," مقابل حالة pagination قديمة في منتصف إعادة الضبط، فيخرج طلب بـ ",[18,11770,11771],{},"current_page: 2"," مقابل handler يظن نفسه يرجع الصفحة الأولى. هذا الطلب خاطئ جوهرياً، ويجب أن يُمنع لا أن يُصحَّح.",[11,11774,11775,11776,11779],{},"بعدها أمسح items والـ pagination. أُبقي على ",[18,11777,11778],{},"per_page"," لأنها في الغالب تفضيل مستخدم لا ينبغي للفلتر أن يعبث به. أما الحقول الثلاثة الأخرى فتعود إلى قيمها الافتراضية.",[11,11781,11782,11783,11786],{},"ثم أُطلق ",[18,11784,11785],{},"fetchData('init')",". يظهر الـ skeleton، ويعمل الـ fetch بالـ handler الجديد، وتصل العناصر.",[11,11788,11789],{},"وأخيراً أُعيد ربط الـ observer. عقدة الـ DOM نفسها لم تتغيّر، لكنني أربط نسخة observer جديدة بحالة ما بعد إعادة الضبط.",[11,11791,11792,11793,11796,11797,11800,11801,11804],{},"ثمة تفصيلة دقيقة تستحق التدوين هنا. الـ watch يجري على ",[18,11794,11795],{},"props.fetchHandler"," بهوية المرجع. فإن مرّر المستهلك ",[18,11798,11799],{},"() => $api('\u002Fproducts', { query: { category } })"," بشكل inline داخل الـ template، تُنشأ دالة جديدة في كل re-render للأب، وينطلق الـ watch في كل re-render. غالباً هذا ليس ما تريده. على المستهلكين أن ينضبطوا: memoize الـ handler عبر ",[18,11802,11803],{},"computed","، أو أخذه من composable هويته مستقرة، أو القبول بأن القائمة ستعيد الجلب مع كل render للأب.",[11,11806,11807,11808,11810],{},"فكّرتُ في أن أُضيف prop مستقلاً باسم ",[18,11809,7093],{}," لإطلاق إعادة الضبط صراحةً بدلاً من الاعتماد على هوية المرجع، لكن هذا يدفع بالتعقيد إلى كل مستهلك. عقد watch القائم على هوية المرجع أبسط بكثير إن كنت مستعداً لتوضيح فكرة هوية الدالة للفريق مرة واحدة.",[26,11812,11814],{"id":11813},"تنظيف-الـ-observer-والتسريب-الذي-يمنعه","تنظيف الـ observer، والتسريب الذي يمنعه",[11,11816,11817,11818,11820,11821,11823,11824,394,11826,11828],{},"هذه هي المشكلة الهادئة. ينتقل المستخدم من مسار كان يحتوي على ",[18,11819,10554],{},". تُفرِّغ Vue المكوّن. لكن الـ IntersectionObserver يبقى حياً إن لم تنظّفه، محتفظاً بمرجع لعقدة الـ sentinel في الـ DOM، وبـ closure الـ callback الذي التقط نطاق ",[18,11822,10154],{}," كاملاً: ",[18,11825,10257],{},[18,11827,10319],{}," وكل الـ refs ومثيل المكوّن نفسه.",[11,11830,11831],{},"لا شيء من هذا يذهب إلى الـ GC حتى يُفلته الـ observer. وعلى مسار فيه حركة كثيفة دخولاً وخروجاً، يتراكم التسريب. لاحظتُ واحداً كهذا في تطبيق سابق بعد أن أبلغ مستخدم بأن الصفحة تصير أبطأ كلما طال استخدامه لها. جلسته كانت تُجمِّع حالة عشرات المكوّنات الميتة.",[11,11833,11834],{},"الحل استدعاء واحد:",[40,11836,11838],{"className":1893,"code":11837,"language":1895,"meta":45,"style":45},"onBeforeUnmount(() => {\n  observer?.disconnect();\n  observer = null;\n});\n",[18,11839,11840,11851,11860,11871],{"__ignoreMap":45},[49,11841,11842,11845,11847,11849],{"class":51,"line":52},[49,11843,11844],{"class":262},"onBeforeUnmount",[49,11846,645],{"class":55},[49,11848,648],{"class":63},[49,11850,1625],{"class":55},[49,11852,11853,11856,11858],{"class":51,"line":82},[49,11854,11855],{"class":55},"  observer?.",[49,11857,11655],{"class":262},[49,11859,3477],{"class":55},[49,11861,11862,11865,11867,11869],{"class":51,"line":88},[49,11863,11864],{"class":55},"  observer ",[49,11866,67],{"class":63},[49,11868,657],{"class":333},[49,11870,319],{"class":55},[49,11872,11873],{"class":51,"line":94},[49,11874,388],{"class":55},[11,11876,11877,11878,11881,11882,11881,11885,11881,11888,11881,11891,11893,11894,3728,11896,11899],{},"يسهل كتابته، ويسهل نسيانه بنفس القدر. ما يمسك هذا في code review هو قائمة تحقق ذهنية: كل ",[18,11879,11880],{},"new IntersectionObserver","، ",[18,11883,11884],{},"new MutationObserver",[18,11886,11887],{},"addEventListener",[18,11889,11890],{},"setInterval",[18,11892,1227],{}," يستدعي teardown مقابلاً في ",[18,11895,11844],{},[18,11897,11898],{},"onUnmounted",". إن لم تستطع الإشارة إلى الـ teardown، فأنت تُسرّب. في كل مرة.",[1013,11901,11903,11906],{"id":11902},"v-scroll-reveal-observer-لكل-بطاقة",[18,11904,11905],{},"v-scroll-reveal",": observer لكل بطاقة",[11,11908,11909],{},"توجيه scroll-reveal الذي يعمل على مستوى كل بطاقة داخل المكوّن نفسه له نسخته الخاصة من القصة:",[40,11911,11913],{"className":1893,"code":11912,"language":1895,"meta":45,"style":45},"const vScrollReveal = {\n  mounted(el: HTMLElement) {\n    \u002F\u002F ... setup observer, stash on el._scrollObserver\n    observer.observe(el);\n  },\n  unmounted(el: HTMLElement) {\n    const observer = (el as HTMLElement & { _scrollObserver?: IntersectionObserver })._scrollObserver;\n    observer?.disconnect();\n  },\n} as const;\n",[18,11914,11915,11926,11943,11948,11959,11963,11978,12009,12017,12021],{"__ignoreMap":45},[49,11916,11917,11919,11922,11924],{"class":51,"line":52},[49,11918,330],{"class":63},[49,11920,11921],{"class":333}," vScrollReveal",[49,11923,343],{"class":63},[49,11925,1625],{"class":55},[49,11927,11928,11931,11933,11936,11938,11941],{"class":51,"line":82},[49,11929,11930],{"class":262},"  mounted",[49,11932,625],{"class":55},[49,11934,11935],{"class":601},"el",[49,11937,337],{"class":63},[49,11939,11940],{"class":262}," HTMLElement",[49,11942,2149],{"class":55},[49,11944,11945],{"class":51,"line":88},[49,11946,11947],{"class":2472},"    \u002F\u002F ... setup observer, stash on el._scrollObserver\n",[49,11949,11950,11953,11956],{"class":51,"line":94},[49,11951,11952],{"class":55},"    observer.",[49,11954,11955],{"class":262},"observe",[49,11957,11958],{"class":55},"(el);\n",[49,11960,11961],{"class":51,"line":363},[49,11962,2237],{"class":55},[49,11964,11965,11968,11970,11972,11974,11976],{"class":51,"line":374},[49,11966,11967],{"class":262},"  unmounted",[49,11969,625],{"class":55},[49,11971,11935],{"class":601},[49,11973,337],{"class":63},[49,11975,11940],{"class":262},[49,11977,2149],{"class":55},[49,11979,11980,11982,11985,11987,11990,11992,11994,11997,11999,12002,12004,12006],{"class":51,"line":385},[49,11981,2204],{"class":63},[49,11983,11984],{"class":333}," observer",[49,11986,343],{"class":63},[49,11988,11989],{"class":55}," (el ",[49,11991,2657],{"class":63},[49,11993,11940],{"class":262},[49,11995,11996],{"class":63}," &",[49,11998,701],{"class":55},[49,12000,12001],{"class":601},"_scrollObserver",[49,12003,3946],{"class":63},[49,12005,11474],{"class":262},[49,12007,12008],{"class":55}," })._scrollObserver;\n",[49,12010,12011,12013,12015],{"class":51,"line":662},[49,12012,11652],{"class":55},[49,12014,11655],{"class":262},[49,12016,3477],{"class":55},[49,12018,12019],{"class":51,"line":691},[49,12020,2237],{"class":55},[49,12022,12023,12026,12028,12031],{"class":51,"line":696},[49,12024,12025],{"class":55},"} ",[49,12027,2657],{"class":63},[49,12029,12030],{"class":63}," const",[49,12032,319],{"class":55},[11,12034,12035,12036,12039,12040,12043,12044,12047,12048,12050],{},"يخبّئ التوجيه observer الخاص به على العنصر نفسه في ",[18,12037,12038],{},"el._scrollObserver"," كي يجده ",[18,12041,12042],{},"unmounted"," لاحقاً. يوجد أيضاً استدعاء ",[18,12045,12046],{},"observer.unobserve(el)"," داخل callback الـ mount. متى ظهرت البطاقة بالـ fade، لا يبقى شيء يستحق المراقبة، وإبقاء الـ observer مرتبطاً يعني عملاً إضافياً على كل حدث تمرير بلا عائد. أما ",[18,12049,12042],{}," فهو احتياط مضاعف للبطاقات التي أُفرِّغت قبل أن تُرى أصلاً، سواء لأن المستخدم مرّ عليها بسرعة، أو غادر المسار، أو أزالها فلتر.",[11,12052,12053,12054,12056,12057,12060],{},"الـ type assertion على ",[18,12055,12001],{}," أقبح مما أرغب. مفتاح ",[18,12058,12059],{},"Symbol"," كان سيوفّر الـ cast، لكنه سيكسر أيضاً استعراضه في DevTools. اخترتُ النسخة التي أستطيع قراءتها فعلاً.",[26,12062,12064,12065],{"id":12063},"type-guards-من-دون-zod","Type guards من دون ",[18,12066,12067],{},"zod",[11,12069,12070],{},"يبقى شيئان خارج إطار المشكلات الأربع، لكنهما استحقّا مكانهما داخل المكوّن.",[11,12072,12073],{},"أولاً، الـ type guards لبيانات الـ pagination الوصفية:",[40,12075,12077],{"className":1893,"code":12076,"language":1895,"meta":45,"style":45},"function isRecord(value: unknown): value is Record\u003Cstring, unknown> {\n  return typeof value === 'object' && value !== null;\n}\nfunction isNumber(value: unknown): value is number {\n  return typeof value === 'number' && Number.isFinite(value);\n}\nfunction isPaginationMeta(value: unknown): value is ISPPaginationMeta {\n  if (!isRecord(value)) return false;\n  return (\n    isNumber(value.current_page) &&\n    isNumber(value.per_page) &&\n    isNumber(value.total) &&\n    isNumber(value.last_page)\n  );\n}\n",[18,12078,12079,12115,12138,12142,12169,12191,12195,12222,12242,12248,12259,12268,12277,12284,12288],{"__ignoreMap":45},[49,12080,12081,12083,12086,12088,12090,12092,12094,12096,12098,12101,12103,12105,12107,12109,12111,12113],{"class":51,"line":52},[49,12082,2418],{"class":63},[49,12084,12085],{"class":262}," isRecord",[49,12087,625],{"class":55},[49,12089,4892],{"class":601},[49,12091,337],{"class":63},[49,12093,4446],{"class":333},[49,12095,2433],{"class":55},[49,12097,337],{"class":63},[49,12099,12100],{"class":601}," value",[49,12102,6283],{"class":63},[49,12104,5455],{"class":262},[49,12106,56],{"class":55},[49,12108,1914],{"class":333},[49,12110,159],{"class":55},[49,12112,6871],{"class":333},[49,12114,3377],{"class":55},[49,12116,12117,12119,12121,12124,12126,12128,12130,12132,12134,12136],{"class":51,"line":82},[49,12118,2962],{"class":63},[49,12120,3809],{"class":63},[49,12122,12123],{"class":55}," value ",[49,12125,654],{"class":63},[49,12127,8243],{"class":70},[49,12129,8933],{"class":63},[49,12131,12123],{"class":55},[49,12133,3815],{"class":63},[49,12135,657],{"class":333},[49,12137,319],{"class":55},[49,12139,12140],{"class":51,"line":88},[49,12141,1719],{"class":55},[49,12143,12144,12146,12149,12151,12153,12155,12157,12159,12161,12163,12165,12167],{"class":51,"line":94},[49,12145,2418],{"class":63},[49,12147,12148],{"class":262}," isNumber",[49,12150,625],{"class":55},[49,12152,4892],{"class":601},[49,12154,337],{"class":63},[49,12156,4446],{"class":333},[49,12158,2433],{"class":55},[49,12160,337],{"class":63},[49,12162,12100],{"class":601},[49,12164,6283],{"class":63},[49,12166,3775],{"class":333},[49,12168,1625],{"class":55},[49,12170,12171,12173,12175,12177,12179,12182,12184,12186,12188],{"class":51,"line":363},[49,12172,2962],{"class":63},[49,12174,3809],{"class":63},[49,12176,12123],{"class":55},[49,12178,654],{"class":63},[49,12180,12181],{"class":70}," 'number'",[49,12183,8933],{"class":63},[49,12185,6148],{"class":55},[49,12187,6151],{"class":262},[49,12189,12190],{"class":55},"(value);\n",[49,12192,12193],{"class":51,"line":374},[49,12194,1719],{"class":55},[49,12196,12197,12199,12202,12204,12206,12208,12210,12212,12214,12216,12218,12220],{"class":51,"line":385},[49,12198,2418],{"class":63},[49,12200,12201],{"class":262}," isPaginationMeta",[49,12203,625],{"class":55},[49,12205,4892],{"class":601},[49,12207,337],{"class":63},[49,12209,4446],{"class":333},[49,12211,2433],{"class":55},[49,12213,337],{"class":63},[49,12215,12100],{"class":601},[49,12217,6283],{"class":63},[49,12219,10964],{"class":262},[49,12221,1625],{"class":55},[49,12223,12224,12226,12228,12230,12233,12236,12238,12240],{"class":51,"line":662},[49,12225,2449],{"class":63},[49,12227,2183],{"class":55},[49,12229,2454],{"class":63},[49,12231,12232],{"class":262},"isRecord",[49,12234,12235],{"class":55},"(value)) ",[49,12237,2197],{"class":63},[49,12239,3299],{"class":333},[49,12241,319],{"class":55},[49,12243,12244,12246],{"class":51,"line":691},[49,12245,2962],{"class":63},[49,12247,10755],{"class":55},[49,12249,12250,12253,12256],{"class":51,"line":696},[49,12251,12252],{"class":262},"    isNumber",[49,12254,12255],{"class":55},"(value.current_page) ",[49,12257,12258],{"class":63},"&&\n",[49,12260,12261,12263,12266],{"class":51,"line":723},[49,12262,12252],{"class":262},[49,12264,12265],{"class":55},"(value.per_page) ",[49,12267,12258],{"class":63},[49,12269,12270,12272,12275],{"class":51,"line":744},[49,12271,12252],{"class":262},[49,12273,12274],{"class":55},"(value.total) ",[49,12276,12258],{"class":63},[49,12278,12279,12281],{"class":51,"line":768},[49,12280,12252],{"class":262},[49,12282,12283],{"class":55},"(value.last_page)\n",[49,12285,12286],{"class":51,"line":779},[49,12287,7696],{"class":55},[49,12289,12290],{"class":51,"line":784},[49,12291,1719],{"class":55},[11,12293,12294,12295,12297,12298,12301,12302,12305,12306,394,12309,12312,12313,12316],{},"لا ",[18,12296,12067],{},"، ولا ",[18,12299,12300],{},"yup",". ثلاث دوال، وثلاثون سطراً، دون إضافة أي اعتمادية جديدة إلى مكوّن UI. القضية ليست الأداء، بل أن أي مكوّن UI يعتمد على مكتبة validator يُسرِّب هذا الاختيار إلى كل مستهلك. الـ guards المكتوبة يدوياً تُبقي رسم الاعتماديات نظيفاً، ولهذا الشكل من الفحص (أربعة حقول كلها أرقام) فهي أقصر من أي schema. فحص ",[18,12303,12304],{},"Number.isFinite"," مهم هنا. من دونه يجتاز ",[18,12307,12308],{},"NaN",[18,12310,12311],{},"Infinity"," فحص ",[18,12314,12315],{},"typeof === 'number'","، فتنتهي بسلوك \"الصفحة 3 من Infinity\" في مراحل لاحقة.",[11,12318,12319,12320,12323,12324,12327],{},"ثانياً، الـ ",[18,12321,12322],{},"void fetchNextPage()"," داخل callback الـ observer صغير ومقصود. الـ callback ليس async، فالانتظار بـ await لا يفعل شيئاً. والـ floating promises تُوقع أغلب الـ linters. أما ",[18,12325,12326],{},"void"," فهو إشارة صادقة: \"أعرف أن هذا async، وقصدي ألا أنتظره، وقد فكّرت في الأمر.\" كلمة واحدة، والفرق بينها وبين تعليق suppress هو الفرق بين lint نظيف وآخر مقيّد.",[26,12329,12331],{"id":12330},"ملخّص-الدفاعات-الأربع","ملخّص الدفاعات الأربع",[11,12333,12334,12335,12337],{},"على نحو 230 سطراً، هذا المكوّن من النوع الذي تُعيد كتابته في بعد ظهر واحد. ما يجعله يعمل ليس أي قطعة منفصلة (توزيع الـ arity، ",[18,12336,11016],{}," بوصفه حالة discriminated، تنظيف الـ observer، إعادة الضبط بهوية المرجع، الـ type guards المكتوبة يدوياً). كل واحدة منها صغيرة لدرجة أنك لو سمعتها منفصلة لهززتَ رأسك وتابعتَ.",[11,12339,12340],{},"ما جعل هذا المكوّن يتوقف عن كونه مصنعاً للمشكلات هو أنني رأيتُ أنماط الفشل الأربعة وكتبتُ حارساً محدداً لكل واحدة منها. لا مكتبة ولا تجريد، بل حفنة من الدفاعات الصغيرة، دفعتُ ثمن كل واحدة منها بمشكلة شحنتُها في نسخة سابقة. هذا في الغالب هو ما يعنيه \"التقسية\" عملياً: ليست ذكاءً، بل ذاكرة أطول.",[219,12342,12343],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":45,"searchDepth":82,"depth":82,"links":12345},[12346,12347,12349,12353,12354,12358,12360],{"id":10565,"depth":82,"text":10566},{"id":10669,"depth":82,"text":12348},"fetchHandler.length: توزيع بحسب الـ arity",{"id":11013,"depth":82,"text":12350,"children":12351},"inFlight: بوابة واحدة، وثلاث قراءات مشتقة",[12352],{"id":11412,"depth":88,"text":11413},{"id":11602,"depth":82,"text":11603},{"id":11813,"depth":82,"text":11814,"children":12355},[12356],{"id":11902,"depth":88,"text":12357},"v-scroll-reveal: observer لكل بطاقة",{"id":12063,"depth":82,"text":12359},"Type guards من دون zod",{"id":12330,"depth":82,"text":12331},"2026-02-28","أربع مشكلات تظهر عاجلاً أم آجلاً في أيّ قائمة infinite-scroll، والأسطر المحدَّدة داخل مكوّن Vue من 230 سطراً تُعالج كلاً منها.",[12364,1840,12365,4077,12366,12367,12368,1846,1841],"infinite scroll","IntersectionObserver","Pagination","arity dispatch","memory leak",{},"\u002Fwrites-ar\u002Finside-asentinel-pagination",{"title":10534,"description":12362},"writes-ar\u002Finside-asentinel-pagination","VTqiNOPf4SYFIKakGRY0kuC2OO6tUjqtUZOBX36xYQk",{"id":12375,"title":12376,"body":12377,"date":14074,"description":14075,"description_ar":1836,"draft":1837,"extension":1838,"image":1836,"keywords":14076,"lang":1849,"meta":14088,"navigation":324,"path":14089,"seo":14090,"stem":14091,"title_ar":1836,"updatedAt":1836,"__hash__":14092},"writesAr\u002Fwrites-ar\u002Fbuilding-ali-nuxt-toolkit.md","بناء ali-nuxt-toolkit، جولة داخل التفاصيل",{"type":8,"value":12378,"toc":14051},[12379,12385,12388,12392,12397,12430,12433,12459,12462,12466,12469,12738,12753,12761,12768,12779,12876,12883,12890,12893,12931,12934,13035,13042,13046,13068,13103,13106,13110,13113,13177,13184,13202,13209,13215,13365,13386,13393,13396,13506,13509,13573,13580,13587,13590,13597,13751,13769,13776,13875,13882,13886,13889,13897,13900,13904,13914,13917,13953,13957,13968,13976,13991,13995,13998,14038,14048],[11,12380,12381,12382,527],{},"قبل فترة، قررتُ أن أجمع الأنماط التي وجدتُ نفسي أعيد كتابتها في العمل مرة بعد مرة داخل مجموعة صغيرة من موديولات Nuxt. لا شيء مبتكر هنا، فقط الأشياء التي يكتبها في النهاية أي فريق يشتغل على SaaS: عميل HTTP مكتوب بأنواع، وmiddleware مربوط بالـ layouts، وخدمة تشفير للأسرار المخزّنة محلياً. تغليف هذه الأشياء بشكل نظيف تحوّل بحد ذاته إلى مشروع مستقل، وهو ",[36,12383,12384],{},"ali-nuxt-toolkit",[11,12386,12387],{},"هذه التدوينة جولة داخل ما بُني ولماذا اتّخذت بعض التفاصيل الشكل الذي هي عليه. سأتجاوز البديهيات وأُنفق معظم الكلام على التفاصيل التي كنتُ أتمنى قراءتها لو كتب أحد غيري هذه الحزمة.",[26,12389,12391],{"id":12390},"شكل-الـ-repo","شكل الـ repo",[11,12393,12394,12396],{},[18,12395,12384],{}," عبارة عن monorepo مبني على pnpm. المستوى الأعلى تقريباً على هذا الشكل:",[8674,12398,12399,12408,12418,12424],{},[3673,12400,12401,12404,12405,527],{},[18,12402,12403],{},"packages\u002F",": ثلاث موديولات تُنشر بشكل مستقل تحت نطاق ",[18,12406,12407],{},"@alikhalilll",[3673,12409,12410,12413,12414,12417],{},[18,12411,12412],{},"apps\u002Fdocs\u002F",": موقع Nuxt 4 مبني على ",[18,12415,12416],{},"@nuxt\u002Fcontent","، يُولَّد مسبقاً كـ HTML ثابت.",[3673,12419,12420,12423],{},[18,12421,12422],{},"playgrounds\u002Fnuxt\u002F",": تطبيق بسيط يربط الموديولات الثلاثة معاً، مفيد لتجربة كل شيء محلياً بسرعة.",[3673,12425,12426,12429],{},[18,12427,12428],{},".github\u002Fworkflows\u002F",": خط CI (lint وtypecheck وmatrix build على Node 20 و22) وخط إصدار مبني على Changesets.",[11,12431,12432],{},"الحزم الثلاث هي:",[8674,12434,12435,12443,12451],{},[3673,12436,12437,12442],{},[36,12438,12439],{},[18,12440,12441],{},"@alikhalilll\u002Fnuxt-api-provider",": عميل fetch بأنواع صارمة، مع سلسلة interceptors، وإعادة محاولة مع backoff، وtimeouts، وتتبّع تقدّم الرفع والتنزيل.",[3673,12444,12445,12450],{},[36,12446,12447],{},[18,12448,12449],{},"@alikhalilll\u002Fnuxt-auto-middleware",": طبقة route middleware مربوطة بالـ layouts، تدعم أنماط glob ومجموعات مسمّاة وتجاوزات على مستوى الصفحة.",[3673,12452,12453,12458],{},[36,12454,12455],{},[18,12456,12457],{},"@alikhalilll\u002Fnuxt-crypto",": تشفير AES-256-GCM مع PBKDF2 فوق Web Crypto، مع cache من نوع LRU للمفاتيح وخوارزميات قابلة للتبديل.",[11,12460,12461],{},"اخترتُ عمداً أن تكون الحزم صغيرة ومركّزة. كل واحدة تشتغل بمفردها، ولكل واحدة أيضاً نواة مستقلة عن الإطار تعمل داخل Node أو Bun أو Deno أو حتى داخل اختبار، من دون أن تحتاج إلى Nuxt أصلاً.",[26,12463,12465],{"id":12464},"هيكل-الموديول","هيكل الموديول",[11,12467,12468],{},"الموديولات الثلاثة تسير على نفس هيكل Nuxt 4:",[40,12470,12472],{"className":1893,"code":12471,"language":1895,"meta":45,"style":45},"export default defineNuxtModule\u003COptions>({\n  meta: {\n    name,\n    configKey,\n    compatibility: { nuxt: '>=3.0.0' },\n  },\n  defaults: { \u002F* ... *\u002F },\n  setup(options, nuxt) {\n    \u002F\u002F 1. Write a serialized config file into .nuxt\n    addTemplate({\n      filename: 'my-module-config.mjs',\n      getContents: () => `export default ${JSON.stringify(config)};\\n`,\n    });\n\n    \u002F\u002F 2. Register the runtime plugin\n    addPlugin({ src: resolver.resolve('.\u002Fruntime\u002Fplugin'), mode: 'all' });\n\n    \u002F\u002F 3. Augment Nuxt's types so $myModule shows up everywhere\n    const typesTemplate = addTemplate({\n      filename: 'types\u002Fmy-module.d.ts',\n      getContents: () => typeDeclarations,\n    });\n    nuxt.hook('prepare:types', ({ references }) => {\n      references.push({ path: typesTemplate.dst });\n    });\n  },\n});\n",[18,12473,12474,12492,12497,12502,12507,12517,12521,12530,12546,12551,12558,12568,12606,12611,12615,12620,12643,12647,12652,12666,12675,12686,12690,12715,12726,12730,12734],{"__ignoreMap":45},[49,12475,12476,12478,12481,12484,12486,12489],{"class":51,"line":52},[49,12477,3341],{"class":63},[49,12479,12480],{"class":63}," default",[49,12482,12483],{"class":262}," defineNuxtModule",[49,12485,56],{"class":55},[49,12487,12488],{"class":262},"Options",[49,12490,12491],{"class":55},">({\n",[49,12493,12494],{"class":51,"line":82},[49,12495,12496],{"class":55},"  meta: {\n",[49,12498,12499],{"class":51,"line":88},[49,12500,12501],{"class":55},"    name,\n",[49,12503,12504],{"class":51,"line":94},[49,12505,12506],{"class":55},"    configKey,\n",[49,12508,12509,12512,12515],{"class":51,"line":363},[49,12510,12511],{"class":55},"    compatibility: { nuxt: ",[49,12513,12514],{"class":70},"'>=3.0.0'",[49,12516,7633],{"class":55},[49,12518,12519],{"class":51,"line":374},[49,12520,2237],{"class":55},[49,12522,12523,12526,12528],{"class":51,"line":385},[49,12524,12525],{"class":55},"  defaults: { ",[49,12527,6528],{"class":2472},[49,12529,7633],{"class":55},[49,12531,12532,12535,12537,12539,12541,12544],{"class":51,"line":662},[49,12533,12534],{"class":262},"  setup",[49,12536,625],{"class":55},[49,12538,5415],{"class":601},[49,12540,159],{"class":55},[49,12542,12543],{"class":601},"nuxt",[49,12545,2149],{"class":55},[49,12547,12548],{"class":51,"line":691},[49,12549,12550],{"class":2472},"    \u002F\u002F 1. Write a serialized config file into .nuxt\n",[49,12552,12553,12556],{"class":51,"line":696},[49,12554,12555],{"class":262},"    addTemplate",[49,12557,720],{"class":55},[49,12559,12560,12563,12566],{"class":51,"line":723},[49,12561,12562],{"class":55},"      filename: ",[49,12564,12565],{"class":70},"'my-module-config.mjs'",[49,12567,360],{"class":55},[49,12569,12570,12573,12575,12577,12580,12583,12585,12588,12590,12593,12595,12598,12601,12604],{"class":51,"line":744},[49,12571,12572],{"class":262},"      getContents",[49,12574,750],{"class":55},[49,12576,648],{"class":63},[49,12578,12579],{"class":70}," `export default ${",[49,12581,12582],{"class":333},"JSON",[49,12584,527],{"class":70},[49,12586,12587],{"class":262},"stringify",[49,12589,625],{"class":70},[49,12591,12592],{"class":55},"config",[49,12594,2433],{"class":70},[49,12596,12597],{"class":70},"};",[49,12599,12600],{"class":333},"\\n",[49,12602,12603],{"class":70},"`",[49,12605,360],{"class":55},[49,12607,12608],{"class":51,"line":768},[49,12609,12610],{"class":55},"    });\n",[49,12612,12613],{"class":51,"line":779},[49,12614,325],{"emptyLinePlaceholder":324},[49,12616,12617],{"class":51,"line":784},[49,12618,12619],{"class":2472},"    \u002F\u002F 2. Register the runtime plugin\n",[49,12621,12622,12625,12628,12630,12632,12635,12638,12641],{"class":51,"line":789},[49,12623,12624],{"class":262},"    addPlugin",[49,12626,12627],{"class":55},"({ src: resolver.",[49,12629,4238],{"class":262},[49,12631,625],{"class":55},[49,12633,12634],{"class":70},"'.\u002Fruntime\u002Fplugin'",[49,12636,12637],{"class":55},"), mode: ",[49,12639,12640],{"class":70},"'all'",[49,12642,1927],{"class":55},[49,12644,12645],{"class":51,"line":820},[49,12646,325],{"emptyLinePlaceholder":324},[49,12648,12649],{"class":51,"line":829},[49,12650,12651],{"class":2472},"    \u002F\u002F 3. Augment Nuxt's types so $myModule shows up everywhere\n",[49,12653,12654,12656,12659,12661,12664],{"class":51,"line":834},[49,12655,2204],{"class":63},[49,12657,12658],{"class":333}," typesTemplate",[49,12660,343],{"class":63},[49,12662,12663],{"class":262}," addTemplate",[49,12665,720],{"class":55},[49,12667,12668,12670,12673],{"class":51,"line":844},[49,12669,12562],{"class":55},[49,12671,12672],{"class":70},"'types\u002Fmy-module.d.ts'",[49,12674,360],{"class":55},[49,12676,12677,12679,12681,12683],{"class":51,"line":861},[49,12678,12572],{"class":262},[49,12680,750],{"class":55},[49,12682,648],{"class":63},[49,12684,12685],{"class":55}," typeDeclarations,\n",[49,12687,12688],{"class":51,"line":888},[49,12689,12610],{"class":55},[49,12691,12692,12695,12698,12700,12703,12706,12709,12711,12713],{"class":51,"line":906},[49,12693,12694],{"class":55},"    nuxt.",[49,12696,12697],{"class":262},"hook",[49,12699,625],{"class":55},[49,12701,12702],{"class":70},"'prepare:types'",[49,12704,12705],{"class":55},", ({ ",[49,12707,12708],{"class":601},"references",[49,12710,4494],{"class":55},[49,12712,648],{"class":63},[49,12714,1625],{"class":55},[49,12716,12717,12720,12723],{"class":51,"line":927},[49,12718,12719],{"class":55},"      references.",[49,12721,12722],{"class":262},"push",[49,12724,12725],{"class":55},"({ path: typesTemplate.dst });\n",[49,12727,12728],{"class":51,"line":937},[49,12729,12610],{"class":55},[49,12731,12732],{"class":51,"line":2725},[49,12733,2237],{"class":55},[49,12735,12736],{"class":51,"line":2731},[49,12737,388],{"class":55},[11,12739,12740,12741,12744,12745,12748,12749,12752],{},"الجزء المثير للاهتمام هو ",[7077,12742,12743],{},"ما لا أفعله",". لا أُمرر كائن الإعدادات عبر provide\u002Finject في وقت التشغيل، ولا أستورد كود المستخدم مباشرة من ",[18,12746,12747],{},"module.ts",". كل شيء يمر عبر ملفات مُولَّدة داخل ",[18,12750,12751],{},".nuxt",". لهذا الأسلوب فائدتان:",[3670,12754,12755,12758],{},[3673,12756,12757],{},"الـ runtime plugin يبقى ضئيلاً جداً، لأنه يستورد كائن JS عادي من مسار افتراضي، بلا أي عمل عند الإقلاع.",[3673,12759,12760],{},"Tree-shaking يشتغل كما يجب. إذا لم تُستخدم ميزة معينة، فلن يُشار إلى محتوى قالبها أصلاً، وسيسقطها الـ bundle بشكل تلقائي.",[1013,12762,12764,12765],{"id":12763},"الموديولات-الافتراضية-وكيف-تُرضي-tsc","الموديولات الافتراضية، وكيف تُرضي ",[18,12766,12767],{},"tsc",[11,12769,12770,12771,12774,12775,12778],{},"القوالب المُولَّدة لا وجود لها على القرص لحظة تشغيل الـ type-checker، وبالتالي فإن سطراً مثل ",[18,12772,12773],{},"import config from '#build\u002Fapi-provider-config.mjs'"," سيظهر كموديول مفقود بلا مجهود إضافي. لذلك تحتوي كل حزمة على ملف ",[18,12776,12777],{},"nuxt-virtual.d.ts"," يُصرّح بـ stubs مقابلة:",[40,12780,12782],{"className":1893,"code":12781,"language":1895,"meta":45,"style":45},"declare module '#build\u002Fapi-provider-config.mjs' {\n  const config: {\n    baseURL: string;\n    defaultTimeoutMs: number;\n    retry: { attempts: number; baseDelayMs: number };\n  };\n  export default config;\n}\n",[18,12783,12784,12797,12808,12819,12830,12857,12862,12872],{"__ignoreMap":45},[49,12785,12786,12789,12792,12795],{"class":51,"line":52},[49,12787,12788],{"class":63},"declare",[49,12790,12791],{"class":63}," module",[49,12793,12794],{"class":70}," '#build\u002Fapi-provider-config.mjs'",[49,12796,1625],{"class":55},[49,12798,12799,12801,12804,12806],{"class":51,"line":82},[49,12800,2603],{"class":63},[49,12802,12803],{"class":333}," config",[49,12805,337],{"class":63},[49,12807,1625],{"class":55},[49,12809,12810,12813,12815,12817],{"class":51,"line":88},[49,12811,12812],{"class":601},"    baseURL",[49,12814,337],{"class":63},[49,12816,607],{"class":333},[49,12818,319],{"class":55},[49,12820,12821,12824,12826,12828],{"class":51,"line":94},[49,12822,12823],{"class":601},"    defaultTimeoutMs",[49,12825,337],{"class":63},[49,12827,3775],{"class":333},[49,12829,319],{"class":55},[49,12831,12832,12835,12837,12839,12842,12844,12846,12848,12851,12853,12855],{"class":51,"line":363},[49,12833,12834],{"class":601},"    retry",[49,12836,337],{"class":63},[49,12838,701],{"class":55},[49,12840,12841],{"class":601},"attempts",[49,12843,337],{"class":63},[49,12845,3775],{"class":333},[49,12847,3951],{"class":55},[49,12849,12850],{"class":601},"baseDelayMs",[49,12852,337],{"class":63},[49,12854,3775],{"class":333},[49,12856,3960],{"class":55},[49,12858,12859],{"class":51,"line":374},[49,12860,12861],{"class":55},"  };\n",[49,12863,12864,12867,12869],{"class":51,"line":385},[49,12865,12866],{"class":63},"  export",[49,12868,12480],{"class":63},[49,12870,12871],{"class":55}," config;\n",[49,12873,12874],{"class":51,"line":662},[49,12875,1719],{"class":55},[11,12877,12878,12879,12882],{},"بهذا يمر ",[18,12880,12881],{},"tsc --noEmit"," بسلام، ويظل الإكمال التلقائي في المحرر يعمل على حقول الإعدادات المُولَّدة.",[26,12884,12886,12889],{"id":12885},"nuxt-api-provider-interceptors-قابلة-للسلسلة-وناقلَين-اثنين",[18,12887,12888],{},"nuxt-api-provider",": interceptors قابلة للسلسلة وناقلَين اثنين",[11,12891,12892],{},"صمّمتُ الواجهة العامة لعميل الـ API لتكون مسطّحة قدر الإمكان، فأنت تناديه كما تنادي أي دالة:",[40,12894,12896],{"className":1893,"code":12895,"language":1895,"meta":45,"style":45},"const users = await $apiProvider\u003CUser[]>('\u002Fusers', { method: 'GET' });\n",[18,12897,12898],{"__ignoreMap":45},[49,12899,12900,12902,12905,12907,12909,12912,12914,12917,12920,12923,12926,12929],{"class":51,"line":52},[49,12901,330],{"class":63},[49,12903,12904],{"class":333}," users",[49,12906,343],{"class":63},[49,12908,3555],{"class":63},[49,12910,12911],{"class":262}," $apiProvider",[49,12913,56],{"class":55},[49,12915,12916],{"class":262},"User",[49,12918,12919],{"class":55},"[]>(",[49,12921,12922],{"class":70},"'\u002Fusers'",[49,12924,12925],{"class":55},", { method: ",[49,12927,12928],{"class":70},"'GET'",[49,12930,1927],{"class":55},[11,12932,12933],{},"وتُضيف السلوك العرضي عبر ثلاث سلاسل:",[40,12935,12937],{"className":1893,"code":12936,"language":1895,"meta":45,"style":45},"$apiProvider.useRequest((ctx) => {\n  ctx.headers.Authorization = `Bearer ${token}`;\n});\n\n$apiProvider.useResponse((ctx, response) => { \u002F* ... *\u002F });\n$apiProvider.useError((ctx, err) => { \u002F* ... *\u002F });\n",[18,12938,12939,12958,12975,12979,12983,13009],{"__ignoreMap":45},[49,12940,12941,12944,12947,12949,12952,12954,12956],{"class":51,"line":52},[49,12942,12943],{"class":55},"$apiProvider.",[49,12945,12946],{"class":262},"useRequest",[49,12948,801],{"class":55},[49,12950,12951],{"class":601},"ctx",[49,12953,807],{"class":55},[49,12955,648],{"class":63},[49,12957,1625],{"class":55},[49,12959,12960,12963,12965,12968,12971,12973],{"class":51,"line":82},[49,12961,12962],{"class":55},"  ctx.headers.Authorization ",[49,12964,67],{"class":63},[49,12966,12967],{"class":70}," `Bearer ${",[49,12969,12970],{"class":55},"token",[49,12972,739],{"class":70},[49,12974,319],{"class":55},[49,12976,12977],{"class":51,"line":88},[49,12978,388],{"class":55},[49,12980,12981],{"class":51,"line":94},[49,12982,325],{"emptyLinePlaceholder":324},[49,12984,12985,12987,12990,12992,12994,12996,12999,13001,13003,13005,13007],{"class":51,"line":363},[49,12986,12943],{"class":55},[49,12988,12989],{"class":262},"useResponse",[49,12991,801],{"class":55},[49,12993,12951],{"class":601},[49,12995,159],{"class":55},[49,12997,12998],{"class":601},"response",[49,13000,807],{"class":55},[49,13002,648],{"class":63},[49,13004,701],{"class":55},[49,13006,6528],{"class":2472},[49,13008,1927],{"class":55},[49,13010,13011,13013,13016,13018,13020,13022,13025,13027,13029,13031,13033],{"class":51,"line":374},[49,13012,12943],{"class":55},[49,13014,13015],{"class":262},"useError",[49,13017,801],{"class":55},[49,13019,12951],{"class":601},[49,13021,159],{"class":55},[49,13023,13024],{"class":601},"err",[49,13026,807],{"class":55},[49,13028,648],{"class":63},[49,13030,701],{"class":55},[49,13032,6528],{"class":2472},[49,13034,1927],{"class":55},[11,13036,13037,13038,13041],{},"كل استدعاء لـ ",[18,13039,13040],{},"use*"," يُرجع دالة لإلغاء الاشتراك، وهذه نقطة مهمة إذا كنتَ تُسجّل interceptors من داخل مكوّن وتحتاج إلى تنظيفها عند إلغاء تركيبه.",[1013,13043,13045],{"id":13044},"ناقلان-وواجهة-api-واحدة","ناقلان، وواجهة API واحدة",[11,13047,13048,13049,13052,13053,13055,13056,13059,13060,13063,13064,13067],{},"معظم الطلبات تمر عبر ",[18,13050,13051],{},"fetch","، لكن ",[18,13054,13051],{}," لا يكشف عن تقدّم الرفع. جانب ",[18,13057,13058],{},"ReadableStream"," من جسم الطلب مناسب للـ streams، غير أن المتصفحات لا تُعطيك أحداث ",[18,13061,13062],{},"progress"," على مستوى البايت كما يفعل XHR. لذلك عندما يُمرّر المستدعي ",[18,13065,13066],{},"onRequestProgress","، يُبدّل العميل الناقل:",[40,13069,13071],{"className":1893,"code":13070,"language":1895,"meta":45,"style":45},"const transport = ctx.options.onRequestProgress\n  ? createXhrFetch(ctx.options.onRequestProgress)\n  : defaultFetch;\n",[18,13072,13073,13085,13096],{"__ignoreMap":45},[49,13074,13075,13077,13080,13082],{"class":51,"line":52},[49,13076,330],{"class":63},[49,13078,13079],{"class":333}," transport",[49,13081,343],{"class":63},[49,13083,13084],{"class":55}," ctx.options.onRequestProgress\n",[49,13086,13087,13090,13093],{"class":51,"line":82},[49,13088,13089],{"class":63},"  ?",[49,13091,13092],{"class":262}," createXhrFetch",[49,13094,13095],{"class":55},"(ctx.options.onRequestProgress)\n",[49,13097,13098,13100],{"class":51,"line":88},[49,13099,4216],{"class":63},[49,13101,13102],{"class":55}," defaultFetch;\n",[11,13104,13105],{},"الغلاف الخاص بـ XHR يُرجع كائناً على شكل Response، حتى لا يهتم بقية الـ pipeline بكيفية عودة البايتات. هذا النمط، أي \"واجهة API واحدة مع تبديل المحرك تحتها\"، صار نمطي المفضل منذ عامين. يُبقي الميزات الاختيارية اختيارية فعلاً، من دون أن يُفرّع مسار الكود بأكمله.",[1013,13107,13109],{"id":13108},"interceptors-عبر-المسار-لا-عبر-الدالة","interceptors عبر المسار، لا عبر الدالة",[11,13111,13112],{},"خيارات الموديول تبدو هكذا:",[40,13114,13116],{"className":1893,"code":13115,"language":1895,"meta":45,"style":45},"{\n  baseURL: 'https:\u002F\u002Fapi.example.com',\n  onRequestPath:  '~\u002Fapi\u002Fon-request.ts',\n  onSuccessPath:  '~\u002Fapi\u002Fon-response.ts',\n  onErrorPath:    '~\u002Fapi\u002Fon-error.ts',\n}\n",[18,13117,13118,13123,13135,13148,13160,13173],{"__ignoreMap":45},[49,13119,13120],{"class":51,"line":52},[49,13121,13122],{"class":55},"{\n",[49,13124,13125,13128,13130,13133],{"class":51,"line":82},[49,13126,13127],{"class":262},"  baseURL",[49,13129,1633],{"class":55},[49,13131,13132],{"class":70},"'https:\u002F\u002Fapi.example.com'",[49,13134,360],{"class":55},[49,13136,13137,13140,13143,13146],{"class":51,"line":88},[49,13138,13139],{"class":262},"  onRequestPath",[49,13141,13142],{"class":55},":  ",[49,13144,13145],{"class":70},"'~\u002Fapi\u002Fon-request.ts'",[49,13147,360],{"class":55},[49,13149,13150,13153,13155,13158],{"class":51,"line":94},[49,13151,13152],{"class":262},"  onSuccessPath",[49,13154,13142],{"class":55},[49,13156,13157],{"class":70},"'~\u002Fapi\u002Fon-response.ts'",[49,13159,360],{"class":55},[49,13161,13162,13165,13168,13171],{"class":51,"line":363},[49,13163,13164],{"class":262},"  onErrorPath",[49,13166,13167],{"class":55},":    ",[49,13169,13170],{"class":70},"'~\u002Fapi\u002Fon-error.ts'",[49,13172,360],{"class":55},[49,13174,13175],{"class":51,"line":374},[49,13176,1719],{"class":55},[11,13178,13179,13180,13183],{},"الـ interceptors تُحلّ بوصفها ",[7077,13181,13182],{},"مسارات ملفات","، لا دوال داخلية. القالب المُولَّد يستوردها ديناميكياً، ويتكفّل الـ runtime plugin بربط ما تُصدّره في السلسلة. أفضّل ذلك لسببين:",[8674,13185,13186,13196],{},[3673,13187,13188,13191,13192,13195],{},[36,13189,13190],{},"لا إعدادات دائرية."," كثيراً ما يحتاج المستخدم إلى استيراد أنواع من موديول api-provider داخل الـ interceptor الخاص به. لو أن الـ interceptor عاش داخل ",[18,13193,13194],{},"nuxt.config.ts","، لأصبح ملف الإعدادات معتمداً على الموديول الذي يقوم بإعداده أصلاً.",[3673,13197,13198,13201],{},[36,13199,13200],{},"Code-splitting."," يُصبح كل interceptor في chunk مستقل، وهذا فرق ملموس عند التحميل البارد.",[1013,13203,13205,13206],{"id":13204},"وسم-الأخطاء-بلا-instanceof","وسم الأخطاء بلا ",[18,13207,13208],{},"instanceof",[11,13210,13211,13212,13214],{},"يفشل ",[18,13213,13208],{}," في اللحظة التي تنتهي فيها إلى نسختين من نفس الصنف، وهذا يحصل مع الاعتماديات المكرّرة، وiframes، وweb workers، وكل غرابة hoisting في pnpm. لذلك يعتمد نوع الخطأ في العميل على وسم Symbol بدلاً من ذلك:",[40,13216,13218],{"className":1893,"code":13217,"language":1895,"meta":45,"style":45},"const API_ERROR_BRAND: unique symbol = Symbol.for(\n  '@alikhalilll\u002Fnuxt-api-provider.ApiError'\n);\n\nexport class ApiError extends Error {\n  readonly [API_ERROR_BRAND] = true;\n\n  static is(e: unknown): e is ApiError {\n    return typeof e === 'object' && e !== null && API_ERROR_BRAND in e;\n  }\n}\n",[18,13219,13220,13245,13250,13254,13258,13275,13294,13298,13327,13357,13361],{"__ignoreMap":45},[49,13221,13222,13224,13227,13229,13232,13235,13237,13240,13243],{"class":51,"line":52},[49,13223,330],{"class":63},[49,13225,13226],{"class":333}," API_ERROR_BRAND",[49,13228,337],{"class":63},[49,13230,13231],{"class":262}," unique",[49,13233,13234],{"class":333}," symbol",[49,13236,343],{"class":63},[49,13238,13239],{"class":55}," Symbol.",[49,13241,13242],{"class":262},"for",[49,13244,2114],{"class":55},[49,13246,13247],{"class":51,"line":82},[49,13248,13249],{"class":70},"  '@alikhalilll\u002Fnuxt-api-provider.ApiError'\n",[49,13251,13252],{"class":51,"line":88},[49,13253,630],{"class":55},[49,13255,13256],{"class":51,"line":94},[49,13257,325],{"emptyLinePlaceholder":324},[49,13259,13260,13262,13264,13267,13270,13273],{"class":51,"line":363},[49,13261,3341],{"class":63},[49,13263,999],{"class":63},[49,13265,13266],{"class":262}," ApiError",[49,13268,13269],{"class":63}," extends",[49,13271,13272],{"class":262}," Error",[49,13274,1625],{"class":55},[49,13276,13277,13280,13282,13285,13288,13290,13292],{"class":51,"line":374},[49,13278,13279],{"class":63},"  readonly",[49,13281,7654],{"class":55},[49,13283,13284],{"class":333},"API_ERROR_BRAND",[49,13286,13287],{"class":55},"] ",[49,13289,67],{"class":63},[49,13291,3174],{"class":333},[49,13293,319],{"class":55},[49,13295,13296],{"class":51,"line":385},[49,13297,325],{"emptyLinePlaceholder":324},[49,13299,13300,13303,13305,13307,13310,13312,13314,13316,13318,13321,13323,13325],{"class":51,"line":662},[49,13301,13302],{"class":63},"  static",[49,13304,6283],{"class":262},[49,13306,625],{"class":55},[49,13308,13309],{"class":601},"e",[49,13311,337],{"class":63},[49,13313,4446],{"class":333},[49,13315,2433],{"class":55},[49,13317,337],{"class":63},[49,13319,13320],{"class":601}," e",[49,13322,6283],{"class":63},[49,13324,13266],{"class":262},[49,13326,1625],{"class":55},[49,13328,13329,13331,13333,13336,13338,13340,13342,13344,13346,13348,13350,13352,13354],{"class":51,"line":691},[49,13330,2945],{"class":63},[49,13332,3809],{"class":63},[49,13334,13335],{"class":55}," e ",[49,13337,654],{"class":63},[49,13339,8243],{"class":70},[49,13341,8933],{"class":63},[49,13343,13335],{"class":55},[49,13345,3815],{"class":63},[49,13347,657],{"class":333},[49,13349,8933],{"class":63},[49,13351,13226],{"class":333},[49,13353,8456],{"class":63},[49,13355,13356],{"class":55}," e;\n",[49,13358,13359],{"class":51,"line":696},[49,13360,2589],{"class":55},[49,13362,13363],{"class":51,"line":723},[49,13364,1719],{"class":55},[11,13366,13367,13368,8698,13371,13374,13375,13378,13379,13382,13383,1251],{},"يمنحك ",[18,13369,13370],{},"Symbol.for",[7077,13372,13373],{},"نفس"," الرمز عبر كل نسخ الموديول، وبالتالي يعمل ",[18,13376,13377],{},"ApiError.is(err)"," في الحالات التي يفشل فيها ",[18,13380,13381],{},"err instanceof ApiError",". هذه الحيلة أنقذتني في كل مشروع اضطر إلى عبور حدود بيئة تنفيذ (",[18,13384,13385],{},"realm boundary",[26,13387,13389,13392],{"id":13388},"nuxt-auto-middleware-regex-عند-التجميع-dispatch-عند-التشغيل",[18,13390,13391],{},"nuxt-auto-middleware",": regex عند التجميع، dispatch عند التشغيل",[11,13394,13395],{},"يستقبل الموديول قواعد على هذه الشاكلة:",[40,13397,13399],{"className":1893,"code":13398,"language":1895,"meta":45,"style":45},"autoMiddleware: {\n  groups: {\n    adminOnly: ['auth', 'require-admin'],\n  },\n  rules: [\n    { layouts: ['admin-*'], middlewares: ['@adminOnly'] },\n    { layouts: [\u002F^workspace\\\u002F.*\u002F], middlewares: ['auth', 'workspace'] },\n  ],\n}\n",[18,13400,13401,13409,13416,13435,13439,13447,13464,13497,13502],{"__ignoreMap":45},[49,13402,13403,13406],{"class":51,"line":52},[49,13404,13405],{"class":262},"autoMiddleware",[49,13407,13408],{"class":55},": {\n",[49,13410,13411,13414],{"class":51,"line":82},[49,13412,13413],{"class":262},"  groups",[49,13415,13408],{"class":55},[49,13417,13418,13421,13424,13427,13429,13432],{"class":51,"line":88},[49,13419,13420],{"class":262},"    adminOnly",[49,13422,13423],{"class":55},": [",[49,13425,13426],{"class":70},"'auth'",[49,13428,159],{"class":55},[49,13430,13431],{"class":70},"'require-admin'",[49,13433,13434],{"class":55},"],\n",[49,13436,13437],{"class":51,"line":94},[49,13438,2237],{"class":55},[49,13440,13441,13444],{"class":51,"line":363},[49,13442,13443],{"class":262},"  rules",[49,13445,13446],{"class":55},": [\n",[49,13448,13449,13452,13455,13458,13461],{"class":51,"line":374},[49,13450,13451],{"class":55},"    { layouts: [",[49,13453,13454],{"class":70},"'admin-*'",[49,13456,13457],{"class":55},"], middlewares: [",[49,13459,13460],{"class":70},"'@adminOnly'",[49,13462,13463],{"class":55},"] },\n",[49,13465,13466,13468,13470,13472,13476,13479,13481,13484,13486,13488,13490,13492,13495],{"class":51,"line":385},[49,13467,13451],{"class":55},[49,13469,4011],{"class":70},[49,13471,4002],{"class":63},[49,13473,13475],{"class":13474},"sA_wV","workspace",[49,13477,13478],{"class":5901},"\\\u002F",[49,13480,527],{"class":333},[49,13482,13483],{"class":63},"*",[49,13485,4011],{"class":70},[49,13487,13457],{"class":55},[49,13489,13426],{"class":70},[49,13491,159],{"class":55},[49,13493,13494],{"class":70},"'workspace'",[49,13496,13463],{"class":55},[49,13498,13499],{"class":51,"line":662},[49,13500,13501],{"class":55},"  ],\n",[49,13503,13504],{"class":51,"line":691},[49,13505,1719],{"class":55},[11,13507,13508],{},"عند setup الموديول، يُترجَم كل glob إلى RegExp، وتُوسَّع كل مرجعية مجموعة، ثم تُسلسَل القواعد الناتجة داخل قالب مُولَّد:",[40,13510,13512],{"className":1893,"code":13511,"language":1895,"meta":45,"style":45},"export const rules = [\n  { patterns: ['^admin-.*$'], middlewares: ['auth', 'require-admin'] },\n  { patterns: ['^workspace\\\\\u002F.*'], middlewares: ['auth', 'workspace'] },\n];\n",[18,13513,13514,13528,13546,13569],{"__ignoreMap":45},[49,13515,13516,13518,13520,13523,13525],{"class":51,"line":52},[49,13517,3341],{"class":63},[49,13519,12030],{"class":63},[49,13521,13522],{"class":333}," rules",[49,13524,343],{"class":63},[49,13526,13527],{"class":55}," [\n",[49,13529,13530,13533,13536,13538,13540,13542,13544],{"class":51,"line":82},[49,13531,13532],{"class":55},"  { patterns: [",[49,13534,13535],{"class":70},"'^admin-.*$'",[49,13537,13457],{"class":55},[49,13539,13426],{"class":70},[49,13541,159],{"class":55},[49,13543,13431],{"class":70},[49,13545,13463],{"class":55},[49,13547,13548,13550,13553,13556,13559,13561,13563,13565,13567],{"class":51,"line":88},[49,13549,13532],{"class":55},[49,13551,13552],{"class":70},"'^workspace",[49,13554,13555],{"class":333},"\\\\",[49,13557,13558],{"class":70},"\u002F.*'",[49,13560,13457],{"class":55},[49,13562,13426],{"class":70},[49,13564,159],{"class":55},[49,13566,13494],{"class":70},[49,13568,13463],{"class":55},[49,13570,13571],{"class":51,"line":94},[49,13572,5961],{"class":55},[11,13574,13575,13576,13579],{},"عند التشغيل، يُعيد الـ plugin بناء الأنماط عبر ",[18,13577,13578],{},"new RegExp(source)"," ويطابقها مع الـ layout الحالي. العميل لا يرى محلّل glob من الأصل، لأنه صار مُجمَّعاً مسبقاً وخارج الصورة. في تطبيق نموذجي، يوفّر هذا بضعة KB، لكن الأهم أن إضافة قواعد جديدة لا يكلّف من حجم الـ bundle سوى نصوص القواعد نفسها.",[26,13581,13583,13586],{"id":13582},"nuxt-crypto-الـ-lru-الذي-يُخزّن-الوعود",[18,13584,13585],{},"nuxt-crypto",": الـ LRU الذي يُخزّن الوعود",[11,13588,13589],{},"خدمة التشفير هي الجزء الذي أنا أكثر رضاً عنه. تُغلّف عناصر AES-GCM وPBKDF2 من Web Crypto بواجهة encrypt\u002Fdecrypt نظيفة. لكن الحيلة الحقيقية هي في cache المفاتيح.",[11,13591,13592,13593,13596],{},"PBKDF2 مع مئة ألف تكرار بطيء عن قصد، وهذا كامل الغرض منه. لكن حين تحاول واجهتك فك تشفير ثلاثة حقول من IndexedDB بالتوازي، فإن اشتقاق ثلاثة مفاتيح منفصلة يكون إهداراً وبطئاً بلا مبرر. الـ cache يحلّ هذا، غير أن التفصيل الذي يُحدث الفرق فعلاً هو ",[7077,13594,13595],{},"ما"," الذي يُخزّنه:",[40,13598,13600],{"className":1893,"code":13599,"language":1895,"meta":45,"style":45},"const getDerivedKey = async (salt, fingerprint?) => {\n  const key = KeyCache.key(salt, iterations, fingerprint);\n  const cached = cache.get(key);\n  if (cached) return cached;\n\n  \u002F\u002F Cache the promise, not the settled key\n  const pending = algorithm.deriveKey({\n    subtle,\n    passphrase,\n    fingerprint,\n    salt,\n    iterations,\n  });\n  cache.set(key, pending);\n  return pending;\n};\n",[18,13601,13602,13631,13648,13665,13675,13679,13684,13701,13706,13711,13716,13721,13726,13730,13740,13747],{"__ignoreMap":45},[49,13603,13604,13606,13609,13611,13613,13615,13618,13620,13623,13625,13627,13629],{"class":51,"line":52},[49,13605,330],{"class":63},[49,13607,13608],{"class":262}," getDerivedKey",[49,13610,343],{"class":63},[49,13612,3344],{"class":63},[49,13614,2183],{"class":55},[49,13616,13617],{"class":601},"salt",[49,13619,159],{"class":55},[49,13621,13622],{"class":601},"fingerprint",[49,13624,758],{"class":63},[49,13626,807],{"class":55},[49,13628,648],{"class":63},[49,13630,1625],{"class":55},[49,13632,13633,13635,13638,13640,13643,13645],{"class":51,"line":82},[49,13634,2603],{"class":63},[49,13636,13637],{"class":333}," key",[49,13639,343],{"class":63},[49,13641,13642],{"class":55}," KeyCache.",[49,13644,7093],{"class":262},[49,13646,13647],{"class":55},"(salt, iterations, fingerprint);\n",[49,13649,13650,13652,13654,13656,13659,13662],{"class":51,"line":88},[49,13651,2603],{"class":63},[49,13653,3469],{"class":333},[49,13655,343],{"class":63},[49,13657,13658],{"class":55}," cache.",[49,13660,13661],{"class":262},"get",[49,13663,13664],{"class":55},"(key);\n",[49,13666,13667,13669,13671,13673],{"class":51,"line":94},[49,13668,2449],{"class":63},[49,13670,3484],{"class":55},[49,13672,2197],{"class":63},[49,13674,3489],{"class":55},[49,13676,13677],{"class":51,"line":363},[49,13678,325],{"emptyLinePlaceholder":324},[49,13680,13681],{"class":51,"line":374},[49,13682,13683],{"class":2472},"  \u002F\u002F Cache the promise, not the settled key\n",[49,13685,13686,13688,13691,13693,13696,13699],{"class":51,"line":385},[49,13687,2603],{"class":63},[49,13689,13690],{"class":333}," pending",[49,13692,343],{"class":63},[49,13694,13695],{"class":55}," algorithm.",[49,13697,13698],{"class":262},"deriveKey",[49,13700,720],{"class":55},[49,13702,13703],{"class":51,"line":662},[49,13704,13705],{"class":55},"    subtle,\n",[49,13707,13708],{"class":51,"line":691},[49,13709,13710],{"class":55},"    passphrase,\n",[49,13712,13713],{"class":51,"line":696},[49,13714,13715],{"class":55},"    fingerprint,\n",[49,13717,13718],{"class":51,"line":723},[49,13719,13720],{"class":55},"    salt,\n",[49,13722,13723],{"class":51,"line":744},[49,13724,13725],{"class":55},"    iterations,\n",[49,13727,13728],{"class":51,"line":768},[49,13729,8036],{"class":55},[49,13731,13732,13735,13737],{"class":51,"line":779},[49,13733,13734],{"class":55},"  cache.",[49,13736,8829],{"class":262},[49,13738,13739],{"class":55},"(key, pending);\n",[49,13741,13742,13744],{"class":51,"line":784},[49,13743,2962],{"class":63},[49,13745,13746],{"class":55}," pending;\n",[49,13748,13749],{"class":51,"line":789},[49,13750,5759],{"class":55},[11,13752,13753,13754,13757,13758,13761,13762,13765,13766,13768],{},"الـ cache يحتفظ بـ ",[18,13755,13756],{},"Promise\u003CCryptoKey>"," وليس بـ ",[18,13759,13760],{},"CryptoKey",". إذا وصلت ثلاث نداءات ",[18,13763,13764],{},"decrypt()"," في نفس الـ tick وبنفس الـ salt، فإنها تنتظر جميعاً ",[7077,13767,13373],{}," الوعد المعلّق، ويعمل PBKDF2 مرة واحدة فقط. أما النسخة الساذجة، أي تلك التي تُخزّن المفتاح الجاهز، فتترك نافذة زمنية يرى فيها استدعاءان أن الـ cache فارغ ويبدآن نفس العمل بالتوازي.",[11,13770,13771,13772,13775],{},"الـ LRU نفسه يستفيد من كون ",[18,13773,13774],{},"Map"," في JavaScript يحتفظ بترتيب الإدراج:",[40,13777,13779],{"className":1893,"code":13778,"language":1895,"meta":45,"style":45},"get(key: string): Promise\u003CCryptoKey> | undefined {\n  const value = this.map.get(key);\n  if (!value) return undefined;\n  this.map.delete(key);   \u002F\u002F delete + re-insert moves to newest\n  this.map.set(key, value);\n  return value;\n}\n",[18,13780,13781,13803,13820,13837,13853,13864,13871],{"__ignoreMap":45},[49,13782,13783,13785,13788,13791,13793,13795,13797,13799,13801],{"class":51,"line":52},[49,13784,13661],{"class":262},[49,13786,13787],{"class":55},"(key: string): ",[49,13789,13790],{"class":333},"Promise",[49,13792,56],{"class":63},[49,13794,13760],{"class":55},[49,13796,5443],{"class":63},[49,13798,679],{"class":63},[49,13800,5440],{"class":333},[49,13802,1625],{"class":55},[49,13804,13805,13807,13809,13811,13813,13816,13818],{"class":51,"line":82},[49,13806,2603],{"class":63},[49,13808,12100],{"class":333},[49,13810,343],{"class":63},[49,13812,6620],{"class":333},[49,13814,13815],{"class":55},".map.",[49,13817,13661],{"class":262},[49,13819,13664],{"class":55},[49,13821,13822,13824,13826,13828,13831,13833,13835],{"class":51,"line":88},[49,13823,2449],{"class":63},[49,13825,2183],{"class":55},[49,13827,2454],{"class":63},[49,13829,13830],{"class":55},"value) ",[49,13832,2197],{"class":63},[49,13834,5440],{"class":333},[49,13836,319],{"class":55},[49,13838,13839,13842,13844,13847,13850],{"class":51,"line":94},[49,13840,13841],{"class":333},"  this",[49,13843,13815],{"class":55},[49,13845,13846],{"class":262},"delete",[49,13848,13849],{"class":55},"(key);   ",[49,13851,13852],{"class":2472},"\u002F\u002F delete + re-insert moves to newest\n",[49,13854,13855,13857,13859,13861],{"class":51,"line":363},[49,13856,13841],{"class":333},[49,13858,13815],{"class":55},[49,13860,8829],{"class":262},[49,13862,13863],{"class":55},"(key, value);\n",[49,13865,13866,13868],{"class":51,"line":374},[49,13867,2962],{"class":63},[49,13869,13870],{"class":55}," value;\n",[49,13872,13873],{"class":51,"line":385},[49,13874,1719],{"class":55},[11,13876,13877,13878,13881],{},"وبالتالي يصبح الإخلاء بسيطاً جداً: ",[18,13879,13880],{},"this.map.keys().next().value"," هو الأقدم دائماً.",[1013,13883,13885],{"id":13884},"حمولات-مُصدَّرة-بإصدار","حمولات مُصدَّرة بإصدار",[11,13887,13888],{},"مخرجات التشفير تحمل بايت إصدار في أولها:",[40,13890,13895],{"className":13891,"code":13893,"language":13894},[13892],"language-text","[version: 1B] [salt: 16B] [iv: 12B] [ciphertext: ...]\n","text",[18,13896,13893],{"__ignoreMap":45},[11,13898,13899],{},"عند فك التشفير، تفشل الإصدارات غير المتطابقة مبكراً. هذا من الأشياء التي تُضاف قبل الحاجة إليها فعلاً. يوم أن أرغب في التدوير من AES-GCM إلى خوارزمية ما بعد الكم، ستظل الحمولات القديمة تُفكّ عبر الخوارزمية القديمة، بينما تستخدم الحمولات الجديدة الخوارزمية الجديدة. من دون بايت إصدار، ستجد نفسك عالقاً في التخمين.",[26,13901,13903],{"id":13902},"موقع-الوثائق-كـ-playground-للموديولات","موقع الوثائق كـ playground للموديولات",[11,13905,13906,13909,13910,13913],{},[18,13907,13908],{},"apps\u002Fdocs"," يستخدم الموديولات الثلاثة. يبدو الأمر بديهياً، لكنه محوري: الوثائق هي ",[7077,13911,13912],{},"الطريقة التي أكتشف بها العطب"," قبل أن يكتشفه المستخدمون. إذا كسر تحديث لـ api-provider مثالَ JSON Placeholder داخل موقع الوثائق، تسقط CI عند خطوة البناء مباشرة. لا حاجة إلى مجموعة اختبارات مخصّصة لهذا النوع من الانحدارات.",[11,13915,13916],{},"من الأشياء الأخرى التي أثبتت جدارتها:",[8674,13918,13919,13931,13945],{},[3673,13920,13921,13926,13927,13930],{},[36,13922,13923],{},[18,13924,13925],{},"@tailwindcss\u002Fvite",": لا PostCSS، ولا ",[18,13928,13929],{},"tailwind.config.js","، فقط Vite plugin. ملف إعدادات أقل يعني حياة أهدأ.",[3673,13932,13933,13936,13937,13940,13941,13944],{},[36,13934,13935],{},"التوليد المسبق الثابت إلى GitHub Pages",": إعداد ",[18,13938,13939],{},"github-pages"," في Nitro يُنتج لك ",[18,13942,13943],{},"\u002F.output\u002Fpublic\u002F","، ثم ترفعه Actions مباشرة إلى Pages. بلا خوادم، بلا إبطال cache، بلا مفاجآت.",[3673,13946,13947,13952],{},[36,13948,13949],{},[18,13950,13951],{},"NUXT_PUBLIC_CF_ANALYTICS_TOKEN",": تحليلات Cloudflare Web اختيارية، بلا كوكيز ولا لافتة موافقة ولا رأي مفروض.",[26,13954,13956],{"id":13955},"الإصدار-changesets-حتى-النهاية","الإصدار: Changesets حتى النهاية",[11,13958,13959,13960,13963,13964,13967],{},"كل PR يُعدّل حزمة يتضمّن ملف ",[18,13961,13962],{},".changeset\u002F\u003Cid>.md"," يصف الترقية وسطر changelog. على master، يعمل GitHub Action يُشغّل ",[18,13965,13966],{},"changesets version","، وينتهي إلى أحد أمرين:",[3670,13969,13970,13973],{},[3673,13971,13972],{},"إما فتح PR بعنوان \"Version Packages\" يحمل الإصدارات المرقّاة.",[3673,13974,13975],{},"أو النشر إلى npm مباشرة إذا كانت الإصدارات قد رُقيّت مسبقاً.",[11,13977,13978,13979,13982,13983,13986,13987,13990],{},"هذا هو أقلّ الإعدادات جهداً بين كل ما جرّبت، ويعطيك في المقابل changelogs صادقة، وانضباط semver، ونشراً إلى npm بلا تدخّل يدوي. خط الـ CI نفسه ثلاثة ملفات فقط: ",[18,13980,13981],{},"ci.yml"," للـ lint والـ typecheck وmatrix build، و",[18,13984,13985],{},"release.yml"," للـ changesets، و",[18,13988,13989],{},"commitlint.yml"," لفرض conventional commits على الـ PRs.",[26,13992,13994],{"id":13993},"الأجزاء-التي-سأعيد-استخدامها-غداً","الأجزاء التي سأعيد استخدامها غداً",[11,13996,13997],{},"لو بدأتُ موديول Nuxt جديداً من الصفر، لنقلتُ هذه الأنماط بلا تفكير:",[8674,13999,14000,14010,14020,14026,14032],{},[3673,14001,14002,14009],{},[36,14003,14004,14005,14008],{},"تسلسل الإعدادات إلى ملف ",[18,14006,14007],{},".mjs"," مُولَّد."," يُجنّبك إعادة التقييم في وقت التشغيل، ويُبقي الـ runtime plugin في حدود خمسة عشر سطراً.",[3673,14011,14012,14019],{},[36,14013,14014,14015,14018],{},"stubs من ",[18,14016,14017],{},".d.ts"," للموديولات الافتراضية."," يمنحك typecheck يعمل دون تشغيل خطوة prepare في Nuxt.",[3673,14021,14022,14025],{},[36,14023,14024],{},"أخطاء موسومة بـ Symbol."," ميزة مجانية للموديولات ذات الحركة الخفيفة، ومنقذة فعلاً للتي تعبر حدود بيئات التنفيذ.",[3673,14027,14028,14031],{},[36,14029,14030],{},"caches مفتاحها الوعد."," كلما كانت العملية غير متزامنة ومكلفة، خزّن الوعد لا النتيجة.",[3673,14033,14034,14037],{},[36,14035,14036],{},"نواة مستقلة عن الإطار مع غلاف Nuxt رفيع."," يجعل الكود أسهل في الاختبار، وأسهل في إعادة الاستخدام، وأسهل في الحذف عند الحاجة.",[11,14039,14040,14041,14047],{},"الـ repo مرخّص برخصة MIT ومتاح على ",[6580,14042,14046],{"href":14043,"rel":14044},"https:\u002F\u002Fgithub.com\u002Falikhalilll",[14045],"nofollow","GitHub",". إذا لاحظت شيئاً يمكن تحسينه، افتح issue. أُفضّل أن يُصحّحني أحدهم على أن أظل مرتاحاً في مكاني.",[219,14049,14050],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}",{"title":45,"searchDepth":82,"depth":82,"links":14052},[14053,14054,14058,14065,14067,14071,14072,14073],{"id":12390,"depth":82,"text":12391},{"id":12464,"depth":82,"text":12465,"children":14055},[14056],{"id":12763,"depth":88,"text":14057},"الموديولات الافتراضية، وكيف تُرضي tsc",{"id":12885,"depth":82,"text":14059,"children":14060},"nuxt-api-provider: interceptors قابلة للسلسلة وناقلَين اثنين",[14061,14062,14063],{"id":13044,"depth":88,"text":13045},{"id":13108,"depth":88,"text":13109},{"id":13204,"depth":88,"text":14064},"وسم الأخطاء بلا instanceof",{"id":13388,"depth":82,"text":14066},"nuxt-auto-middleware: regex عند التجميع، dispatch عند التشغيل",{"id":13582,"depth":82,"text":14068,"children":14069},"nuxt-crypto: الـ LRU الذي يُخزّن الوعود",[14070],{"id":13884,"depth":88,"text":13885},{"id":13902,"depth":82,"text":13903},{"id":13955,"depth":82,"text":13956},{"id":13993,"depth":82,"text":13994},"2026-01-12","ثلاث موديولات Nuxt 4 داخل monorepo مبني بـ pnpm. عميل fetch مكتوب بأنواع صارمة يدعم تقدّم الرفع، وتشفير AES-GCM مع PBKDF2، وطبقة middleware مربوطة بالـ layouts.",[14077,14078,1846,14079,14080,14081,14082,14083,14084,14085,14086,14087],"Nuxt 4","موديولات Nuxt","monorepo بـ pnpm","عميل fetch","Interceptors","تقدّم الرفع","AES-GCM","PBKDF2","Web Crypto","route middleware","Changesets",{},"\u002Fwrites-ar\u002Fbuilding-ali-nuxt-toolkit",{"title":12376,"description":14075},"writes-ar\u002Fbuilding-ali-nuxt-toolkit","SqZJ5rwloniEY3jw7jxe_Dp8J8VS9jpxscQ5Hk_0PSU",{"id":14094,"title":14095,"body":14096,"date":18948,"description":18949,"description_ar":1836,"draft":1837,"extension":1838,"image":1836,"keywords":18950,"lang":1849,"meta":18958,"navigation":324,"path":18959,"seo":18960,"stem":18961,"title_ar":1836,"updatedAt":18948,"__hash__":18962},"writesAr\u002Fwrites-ar\u002Fupload-and-download-progress-in-the-browser.md","غلاف مخصَّص لـ fetch، وXHR لتقدُّم الرفع",{"type":8,"value":14097,"toc":18924},[14098,14114,14132,14142,14145,14164,14171,14178,14181,14206,14221,14244,14267,14299,14312,14320,14324,14339,14342,14346,14349,14470,14473,14476,14734,14748,14755,14760,14802,14805,14987,15003,15007,15010,15558,15564,15567,15635,15638,15710,15714,15717,15920,15926,15930,15933,16062,16067,16070,16077,16083,16244,16250,16270,16273,16277,16280,16427,16430,16451,16464,16477,16488,16497,16556,16585,16588,16693,16701,16705,16708,17101,17104,17147,17151,17157,17493,17516,17534,17540,17549,17561,17564,18349,18352,18365,18385,18593,18617,18650,18654,18669,18749,18762,18779,18783,18786,18799,18813,18824,18839,18856,18860,18863,18918,18921],[11,14099,14100,14101,14103,14104,14106,14107,394,14110,14113],{},"كلما احتجتُ إلى التعامل مع HTTP API من المتصفح، أبدأ بـ ",[18,14102,13051],{}," مباشرة. أراه الأداة الصحيحة في هذا المستوى: مبني على Promise منذ البداية، ويتركَّب بسلاسة مع ",[18,14105,4077],{},"، ويتشارك ",[18,14108,14109],{},"Request",[18,14111,14112],{},"Response"," مع Service Workers وواجهة Cache API، ثم إن التوقيع نفسه يعمل في Node 18 وما بعده، وفي Bun وDeno وCloudflare Workers دون الحاجة إلى محوِّل. وأغلب النقاشات التي تدور حول \"أيّ عميل HTTP أختار؟\" تنتهي عند هذه النقطة.",[11,14115,14116,14117,14119,14120,14122,14123,14125,14126,14128,14129,14131],{},"لكن في ",[18,14118,13051],{}," ثغرة واحدة لا يذكرها أحد، وليست من النوع الذي يتوقَّع أن يُصلَح لاحقاً، لأنها ثغرة بنيوية. ",[18,14121,13051],{}," لا يُطلق أحداث تقدُّم الرفع أصلاً. صحيح أن جسم ",[18,14124,14109],{}," يقبل ",[18,14127,13058],{},"، لكن المتصفح لا يُطلق حدث ",[18,14130,13062],{}," وهو يستنزف البايتات منه إلى الشبكة. تُسلِّم الجسم إلى طبقة الشبكة، ثم يمضي في طريقه، ولا تسمع منه شيئاً بعد ذلك.",[11,14133,14134,14135,14138,14139,527],{},"ولو أردتَ شريط تقدُّم رفع حقيقياً لا مجرَّد spinner وهمي، فليس أمامك سوى ",[18,14136,14137],{},"XMLHttpRequest",". هو الواجهة الأقدم، لكنه الوحيد الذي يكشف عن ",[18,14140,14141],{},"xhr.upload.onprogress",[11,14143,14144],{},"سأتحدَّث في هذه التدوينة عن ثلاثة أمور بالترتيب:",[3670,14146,14147,14153,14158],{},[3673,14148,14149,14150,14152],{},"لماذا يستحق ",[18,14151,13051],{}," الأصلي أن يكون الخيار الافتراضي، وما الفوائد التي تحصل عليها بالضبط من ورائه.",[3673,14154,14155,14156,527],{},"كيف تبني غلاف fetch مخصَّصاً (افتراضات، ومهلات، وinterceptors، وإعادة محاولة) دون أن تُضيِّع شكل ",[18,14157,13051],{},[3673,14159,14160,14161,14163],{},"الثغرة في XHR، وخريطة أحداث التقدُّم، وغلاف صغير مبني على XHR يُعيد ",[18,14162,14112],{}," كي تبقى بقية شيفرتك بشكل fetch.",[11,14165,14166,14167,14170],{},"إذا سبق أن تساءلتَ عن سرِّ وجود مصنع باسم ",[18,14168,14169],{},"createClient()"," في كثير من مشاريع الإنتاج، فإجابتك تكمن في القسمين الأول والثاني. أما القسم الثالث فهو الشيء الوحيد الذي يعجز عنه هذا العميل من تلقاء نفسه.",[26,14172,14174,14175,14177],{"id":14173},"لماذا-fetch-الأصلي-هو-البدائي-الافتراضي","لماذا ",[18,14176,13051],{}," الأصلي هو البدائي الافتراضي",[11,14179,14180],{},"كل نقاش خضتُه حول عملاء HTTP انتهى في نهاية المطاف إلى \"استخدم fetch\". والأسباب تستحق أن تُذكر صراحة، لأنها تحديداً ما ستتنازل عنه لو اخترتَ مكتبة أخرى، ولأنها تتراكم فوق بعضها بمجرَّد أن تبدأ بتغليفه.",[11,14182,14183,14186,14187,14190,14191,14194,14195,14198,14199,14202,14203,14205],{},[36,14184,14185],{},"Promise واحدة، وResponse واحدة."," يُعيد ",[18,14188,14189],{},"fetch(url, init)"," قيمة من نوع ",[18,14192,14193],{},"Promise\u003CResponse>"," مباشرة. لا آلة حالات، ولا ",[18,14196,14197],{},"readyState","، ولا حاجة لربط معالجات أحداث في المسار السليم. تقرأ ",[18,14200,14201],{},"await fetch(...)"," كما تقرأ أي استدعاء غير متزامن آخر في مشروعك. XHR لا يزال مدفوعاً بالأحداث خلف الكواليس، بينما ",[18,14204,13051],{}," هو الواجهة المصمَّمة أصلاً على شكل Promise.",[11,14207,14208,8698,14211,14213,14214,14216,14217,14220],{},[36,14209,14210],{},"بدائيات موحَّدة لـ Request وResponse.",[18,14212,14112],{}," الذي يصلك من ",[18,14215,13051],{}," هو النوع نفسه الذي يُعيده Service Worker، وتخزِّنه Cache API، وتقبله بيئات الحافة. تستطيع أن تستدعي ",[18,14218,14219],{},".clone()"," عليه، أو أن تُعيده إلى المتصفح، أو تخزِّنه، أو تدفعه إلى مكان آخر، دون أي طبقة ترجمة بينهما.",[11,14222,14223,14228,14229,14232,14233,14236,14237,394,14240,14243],{},[36,14224,14225,14226,527],{},"الإلغاء عبر ",[18,14227,4077],{}," إشارة واحدة كافية لإلغاء سلسلة عمل كاملة: fetch، وحساب معتمد عليه، وإعادة محاولة لم تنطلق بعد. تُجمِّع ",[18,14230,14231],{},"AbortSignal.any([...])"," عدة إشارات في إشارة واحدة، وتمنحك ",[18,14234,14235],{},"AbortSignal.timeout(ms)"," مهلة زمنية دون الحاجة إلى رقصة ",[18,14238,14239],{},"setTimeout",[18,14241,14242],{},"clearTimeout",". الإلغاء صار ميزة أصيلة في المنصَّة، لا شأناً تُعالجه كل مكتبة على حدة.",[11,14245,14246,8698,14249,14252,14253,14255,14256,3728,14259,14262,14263,14266],{},[36,14247,14248],{},"أجسام على شكل streams.",[18,14250,14251],{},"Response.body"," هو ",[18,14254,13058],{},". عند التنزيل، تستطيع أن تقرأ القطع فور وصولها، وهذا يكفي لعدَّاد بايتات، أو محلِّل حي، أو لتمرير الـ stream إلى شيء آخر مثل ",[18,14257,14258],{},"createImageBitmap",[18,14260,14261],{},"new Response(stream)"," أو Cache API. ويقبل ",[18,14264,14265],{},"Request.body"," هو الآخر streams، غير أن تقدُّم الرفع هو الحالة الوحيدة التي لا يترجم فيها هذا القبول إلى نتيجة عملية.",[11,14268,14269,14272,14273,394,14276,394,14279,394,14282,394,14284,394,14287,14290,14291,14294,14295,14298],{},[36,14270,14271],{},"خيارات طلب من الدرجة الأولى."," حقول مثل ",[18,14274,14275],{},"credentials",[18,14277,14278],{},"cache",[18,14280,14281],{},"redirect",[18,14283,11146],{},[18,14285,14286],{},"referrer",[18,14288,14289],{},"integrity"," كلها حقول عليا في ",[18,14292,14293],{},"RequestInit"," مباشرة. لا آثار جانبية على شاكلة ",[18,14296,14297],{},"xhr.withCredentials = true","، ولا حيل للالتفاف حول headers ممنوعة. صُمِّمت الواجهة أصلاً وفي ذهنها دلالات الأمن والتخزين المؤقَّت الحديثة.",[11,14300,14301,14304,14305,14307,14308,14311],{},[36,14302,14303],{},"اتساق عبر بيئات التشغيل."," التوقيع نفسه لـ ",[18,14306,13051],{}," يعمل في المتصفحات، وNode 18 وما بعده، وBun وDeno وCloudflare Workers وService Workers. لا يحتاج العميل الذي يعمل في SSR إلى تفريع بـ ",[18,14309,14310],{},"typeof window","، وتستطيع الشيفرة المُصيَّرة على الخادم أن تتشارك مع تلك المُصيَّرة على العميل طبقة HTTP واحدة.",[11,14313,14314,8698,14317,14319],{},[36,14315,14316],{},"قابلية التوسيع دون تسلسلات أصناف.",[18,14318,13051],{}," مجرَّد دالة عادية، وعميلك المخصَّص أيضاً دالة عادية بنفس التوقيع. التغليف، وCurrying، والوسائط، والاستبدال: كل هذه التقنيات القياسية تنطبق كما هي، وهذا بالضبط ما يجعل القسم التالي ممكناً.",[26,14321,14323],{"id":14322},"بناء-غلاف-fetch-مخصَّص","بناء غلاف fetch مخصَّص",[11,14325,14326,14327,14329,14330,3728,14332,14335,14336,14338],{},"قلَّما تجد قاعدة شيفرة إنتاجية تستدعي ",[18,14328,13051],{}," مباشرة. الغالب أن يكون لديها ",[18,14331,14169],{},[18,14333,14334],{},"apiClient"," يضع فوقه طبقات من الافتراضات، وترويسات المصادقة، والمهلات، وإعادة المحاولة، وتوحيد الأخطاء، وربما interceptors أيضاً. والحيلة كلها في أن تبنيه دون أن يفقد شكل ",[18,14337,13051],{},"، حتى إذا احتجتَ لاحقاً أن تُبدِّل الناقل (بناقل مبني على XHR مثلاً كما سنرى) لا تضطر إلى إعادة كتابة كل موضع استدعاء.",[11,14340,14341],{},"سأمرُّ على أربع طبقات، كل واحدة منها صغيرة، وكل واحدة تستحق مكانها.",[1013,14343,14345],{"id":14344},"الطبقة-الأولى-رابط-أساسي-وترويسات-افتراضية-وjson-مُحلَّل","الطبقة الأولى: رابط أساسي، وترويسات افتراضية، وJSON مُحلَّل",[11,14347,14348],{},"ابدأ بأقصر غلاف مفيد يمكن تصوُّره:",[40,14350,14352],{"className":1893,"code":14351,"language":1895,"meta":45,"style":45},"export async function request\u003CT>(path: string, init: RequestInit = {}): Promise\u003CT> {\n  const res = await fetch(path, init);\n  if (!res.ok) throw new ApiError(res.status, await res.text());\n  return res.json() as Promise\u003CT>;\n}\n",[18,14353,14354,14402,14417,14446,14466],{"__ignoreMap":45},[49,14355,14356,14358,14360,14362,14365,14367,14369,14371,14374,14376,14378,14380,14383,14385,14388,14390,14392,14394,14396,14398,14400],{"class":51,"line":52},[49,14357,3341],{"class":63},[49,14359,3344],{"class":63},[49,14361,3347],{"class":63},[49,14363,14364],{"class":262}," request",[49,14366,56],{"class":55},[49,14368,6807],{"class":262},[49,14370,684],{"class":55},[49,14372,14373],{"class":601},"path",[49,14375,337],{"class":63},[49,14377,607],{"class":333},[49,14379,159],{"class":55},[49,14381,14382],{"class":601},"init",[49,14384,337],{"class":63},[49,14386,14387],{"class":262}," RequestInit",[49,14389,343],{"class":63},[49,14391,3365],{"class":55},[49,14393,337],{"class":63},[49,14395,3370],{"class":262},[49,14397,56],{"class":55},[49,14399,6807],{"class":262},[49,14401,3377],{"class":55},[49,14403,14404,14406,14408,14410,14412,14414],{"class":51,"line":82},[49,14405,2603],{"class":63},[49,14407,3881],{"class":333},[49,14409,343],{"class":63},[49,14411,3555],{"class":63},[49,14413,3888],{"class":262},[49,14415,14416],{"class":55},"(path, init);\n",[49,14418,14419,14421,14423,14425,14427,14430,14432,14434,14437,14439,14441,14443],{"class":51,"line":88},[49,14420,2449],{"class":63},[49,14422,2183],{"class":55},[49,14424,2454],{"class":63},[49,14426,3907],{"class":55},[49,14428,14429],{"class":63},"throw",[49,14431,3838],{"class":63},[49,14433,13266],{"class":262},[49,14435,14436],{"class":55},"(res.status, ",[49,14438,3927],{"class":63},[49,14440,3930],{"class":55},[49,14442,13894],{"class":262},[49,14444,14445],{"class":55},"());\n",[49,14447,14448,14450,14452,14454,14456,14458,14460,14462,14464],{"class":51,"line":94},[49,14449,2962],{"class":63},[49,14451,3930],{"class":55},[49,14453,3933],{"class":262},[49,14455,3615],{"class":55},[49,14457,2657],{"class":63},[49,14459,3370],{"class":262},[49,14461,56],{"class":55},[49,14463,6807],{"class":262},[49,14465,6834],{"class":55},[49,14467,14468],{"class":51,"line":363},[49,14469,1719],{"class":55},[11,14471,14472],{},"ثلاثة أسطر تشتري لك سلوكين: رمي الخطأ عند أي استجابة غير 2xx، وتحليل JSON عند النجاح. وبمجرَّد أن تلاحظ نفسك تُكرِّر هذين السلوكين في كل موقع استدعاء، يكون الغلاف قد استحقَّ وجوده.",[11,14474,14475],{},"وشكل أنفع قليلاً يقبل إعداداً:",[40,14477,14479],{"className":1893,"code":14478,"language":1895,"meta":45,"style":45},"export function createClient(options: {\n  baseURL: string;\n  defaultHeaders?: Record\u003Cstring, string>;\n}) {\n  const { baseURL, defaultHeaders = {} } = options;\n\n  return async function request\u003CT>(path: string, init: RequestInit = {}): Promise\u003CT> {\n    const url = new URL(path, baseURL).toString();\n    const headers = { ...defaultHeaders, ...(init.headers as Record\u003Cstring, string>) };\n    const res = await fetch(url, { ...init, headers });\n    if (!res.ok) throw new ApiError(res.status, await res.text());\n    return res.json() as Promise\u003CT>;\n  };\n}\n",[18,14480,14481,14498,14508,14527,14532,14555,14559,14603,14624,14660,14680,14706,14726,14730],{"__ignoreMap":45},[49,14482,14483,14485,14487,14490,14492,14494,14496],{"class":51,"line":52},[49,14484,3341],{"class":63},[49,14486,3347],{"class":63},[49,14488,14489],{"class":262}," createClient",[49,14491,625],{"class":55},[49,14493,5415],{"class":601},[49,14495,337],{"class":63},[49,14497,1625],{"class":55},[49,14499,14500,14502,14504,14506],{"class":51,"line":82},[49,14501,13127],{"class":601},[49,14503,337],{"class":63},[49,14505,607],{"class":333},[49,14507,319],{"class":55},[49,14509,14510,14513,14515,14517,14519,14521,14523,14525],{"class":51,"line":88},[49,14511,14512],{"class":601},"  defaultHeaders",[49,14514,3946],{"class":63},[49,14516,5455],{"class":262},[49,14518,56],{"class":55},[49,14520,1914],{"class":333},[49,14522,159],{"class":55},[49,14524,1914],{"class":333},[49,14526,6834],{"class":55},[49,14528,14529],{"class":51,"line":94},[49,14530,14531],{"class":55},"}) {\n",[49,14533,14534,14536,14538,14541,14543,14546,14548,14551,14553],{"class":51,"line":363},[49,14535,2603],{"class":63},[49,14537,701],{"class":55},[49,14539,14540],{"class":333},"baseURL",[49,14542,159],{"class":55},[49,14544,14545],{"class":333},"defaultHeaders",[49,14547,343],{"class":63},[49,14549,14550],{"class":55}," {} } ",[49,14552,67],{"class":63},[49,14554,6535],{"class":55},[49,14556,14557],{"class":51,"line":374},[49,14558,325],{"emptyLinePlaceholder":324},[49,14560,14561,14563,14565,14567,14569,14571,14573,14575,14577,14579,14581,14583,14585,14587,14589,14591,14593,14595,14597,14599,14601],{"class":51,"line":385},[49,14562,2962],{"class":63},[49,14564,3344],{"class":63},[49,14566,3347],{"class":63},[49,14568,14364],{"class":262},[49,14570,56],{"class":55},[49,14572,6807],{"class":262},[49,14574,684],{"class":55},[49,14576,14373],{"class":601},[49,14578,337],{"class":63},[49,14580,607],{"class":333},[49,14582,159],{"class":55},[49,14584,14382],{"class":601},[49,14586,337],{"class":63},[49,14588,14387],{"class":262},[49,14590,343],{"class":63},[49,14592,3365],{"class":55},[49,14594,337],{"class":63},[49,14596,3370],{"class":262},[49,14598,56],{"class":55},[49,14600,6807],{"class":262},[49,14602,3377],{"class":55},[49,14604,14605,14607,14610,14612,14614,14617,14620,14622],{"class":51,"line":662},[49,14606,2204],{"class":63},[49,14608,14609],{"class":333}," url",[49,14611,343],{"class":63},[49,14613,3838],{"class":63},[49,14615,14616],{"class":262}," URL",[49,14618,14619],{"class":55},"(path, baseURL).",[49,14621,3986],{"class":262},[49,14623,3477],{"class":55},[49,14625,14626,14628,14631,14633,14635,14637,14640,14642,14645,14647,14649,14651,14653,14655,14657],{"class":51,"line":691},[49,14627,2204],{"class":63},[49,14629,14630],{"class":333}," headers",[49,14632,343],{"class":63},[49,14634,701],{"class":55},[49,14636,9696],{"class":63},[49,14638,14639],{"class":55},"defaultHeaders, ",[49,14641,9696],{"class":63},[49,14643,14644],{"class":55},"(init.headers ",[49,14646,2657],{"class":63},[49,14648,5455],{"class":262},[49,14650,56],{"class":55},[49,14652,1914],{"class":333},[49,14654,159],{"class":55},[49,14656,1914],{"class":333},[49,14658,14659],{"class":55},">) };\n",[49,14661,14662,14664,14666,14668,14670,14672,14675,14677],{"class":51,"line":696},[49,14663,2204],{"class":63},[49,14665,3881],{"class":333},[49,14667,343],{"class":63},[49,14669,3555],{"class":63},[49,14671,3888],{"class":262},[49,14673,14674],{"class":55},"(url, { ",[49,14676,9696],{"class":63},[49,14678,14679],{"class":55},"init, headers });\n",[49,14681,14682,14684,14686,14688,14690,14692,14694,14696,14698,14700,14702,14704],{"class":51,"line":723},[49,14683,2138],{"class":63},[49,14685,2183],{"class":55},[49,14687,2454],{"class":63},[49,14689,3907],{"class":55},[49,14691,14429],{"class":63},[49,14693,3838],{"class":63},[49,14695,13266],{"class":262},[49,14697,14436],{"class":55},[49,14699,3927],{"class":63},[49,14701,3930],{"class":55},[49,14703,13894],{"class":262},[49,14705,14445],{"class":55},[49,14707,14708,14710,14712,14714,14716,14718,14720,14722,14724],{"class":51,"line":744},[49,14709,2945],{"class":63},[49,14711,3930],{"class":55},[49,14713,3933],{"class":262},[49,14715,3615],{"class":55},[49,14717,2657],{"class":63},[49,14719,3370],{"class":262},[49,14721,56],{"class":55},[49,14723,6807],{"class":262},[49,14725,6834],{"class":55},[49,14727,14728],{"class":51,"line":768},[49,14729,12861],{"class":55},[49,14731,14732],{"class":51,"line":779},[49,14733,1719],{"class":55},[11,14735,14736,14739,14740,14743,14744,14747],{},[18,14737,14738],{},"new URL(path, baseURL)"," هو الجزء الذي يستحق أن تسرقه. يتعامل مع الشرطات المائلة في البداية والنهاية بشكل صحيح ومنذ اللحظة الأولى، فلا تحتاج إلى تلك اللعبة اليدوية ",[18,14741,14742],{},"${baseURL}\u002F${path}.replace(\u002F\\\u002F+\u002Fg, '\u002F')"," التي ينتهي إليها كل عميل مرتجل مع الوقت. مرِّر مساراً نسبياً فسيُحلَّل مقابل الرابط الأساسي، ومرِّر رابطاً مطلقاً فسيستخدمه ",[18,14745,14746],{},"URL"," كما هو.",[1013,14749,14751,14752],{"id":14750},"الطبقة-الثانية-المهلات-عبر-abortsignal","الطبقة الثانية: المهلات عبر ",[18,14753,14754],{},"AbortSignal",[11,14756,14757,14758,337],{},"الطريقة الصحيحة لتنفيذ المهلات هي ",[18,14759,14235],{},[40,14761,14763],{"className":1893,"code":14762,"language":1895,"meta":45,"style":45},"const signal = AbortSignal.timeout(30_000);\nconst res = await fetch(url, { signal });\n",[18,14764,14765,14787],{"__ignoreMap":45},[49,14766,14767,14769,14772,14774,14777,14780,14782,14785],{"class":51,"line":52},[49,14768,330],{"class":63},[49,14770,14771],{"class":333}," signal",[49,14773,343],{"class":63},[49,14775,14776],{"class":55}," AbortSignal.",[49,14778,14779],{"class":262},"timeout",[49,14781,625],{"class":55},[49,14783,14784],{"class":333},"30_000",[49,14786,630],{"class":55},[49,14788,14789,14791,14793,14795,14797,14799],{"class":51,"line":82},[49,14790,330],{"class":63},[49,14792,3881],{"class":333},[49,14794,343],{"class":63},[49,14796,3555],{"class":63},[49,14798,3888],{"class":262},[49,14800,14801],{"class":55},"(url, { signal });\n",[11,14803,14804],{},"وإن مرَّر المستدعي إشارته الخاصة إلى جانب مهلتك، فادمج الاثنتين بحيث تستطيع أيٌّ منهما إلغاء الطلب:",[40,14806,14808],{"className":1893,"code":14807,"language":1895,"meta":45,"style":45},"function mergeSignals(a?: AbortSignal, b?: AbortSignal): AbortSignal | undefined {\n  if (!a) return b;\n  if (!b) return a;\n  if ('any' in AbortSignal) return AbortSignal.any([a, b]);\n  \u002F\u002F fallback for older runtimes\n  const controller = new AbortController();\n  const onAbort = () => controller.abort();\n  a.addEventListener('abort', onAbort, { once: true });\n  b.addEventListener('abort', onAbort, { once: true });\n  return controller.signal;\n}\n",[18,14809,14810,14846,14862,14878,14902,14907,14921,14940,14959,14976,14983],{"__ignoreMap":45},[49,14811,14812,14814,14817,14819,14821,14823,14826,14828,14830,14832,14834,14836,14838,14840,14842,14844],{"class":51,"line":52},[49,14813,2418],{"class":63},[49,14815,14816],{"class":262}," mergeSignals",[49,14818,625],{"class":55},[49,14820,6580],{"class":601},[49,14822,3946],{"class":63},[49,14824,14825],{"class":262}," AbortSignal",[49,14827,159],{"class":55},[49,14829,6585],{"class":601},[49,14831,3946],{"class":63},[49,14833,14825],{"class":262},[49,14835,2433],{"class":55},[49,14837,337],{"class":63},[49,14839,14825],{"class":262},[49,14841,679],{"class":63},[49,14843,5440],{"class":333},[49,14845,1625],{"class":55},[49,14847,14848,14850,14852,14854,14857,14859],{"class":51,"line":82},[49,14849,2449],{"class":63},[49,14851,2183],{"class":55},[49,14853,2454],{"class":63},[49,14855,14856],{"class":55},"a) ",[49,14858,2197],{"class":63},[49,14860,14861],{"class":55}," b;\n",[49,14863,14864,14866,14868,14870,14873,14875],{"class":51,"line":88},[49,14865,2449],{"class":63},[49,14867,2183],{"class":55},[49,14869,2454],{"class":63},[49,14871,14872],{"class":55},"b) ",[49,14874,2197],{"class":63},[49,14876,14877],{"class":55}," a;\n",[49,14879,14880,14882,14884,14887,14889,14892,14894,14896,14899],{"class":51,"line":94},[49,14881,2449],{"class":63},[49,14883,2183],{"class":55},[49,14885,14886],{"class":70},"'any'",[49,14888,8456],{"class":63},[49,14890,14891],{"class":55}," AbortSignal) ",[49,14893,2197],{"class":63},[49,14895,14776],{"class":55},[49,14897,14898],{"class":262},"any",[49,14900,14901],{"class":55},"([a, b]);\n",[49,14903,14904],{"class":51,"line":363},[49,14905,14906],{"class":2472},"  \u002F\u002F fallback for older runtimes\n",[49,14908,14909,14911,14913,14915,14917,14919],{"class":51,"line":374},[49,14910,2603],{"class":63},[49,14912,3833],{"class":333},[49,14914,343],{"class":63},[49,14916,3838],{"class":63},[49,14918,3841],{"class":262},[49,14920,3477],{"class":55},[49,14922,14923,14925,14928,14930,14932,14934,14936,14938],{"class":51,"line":385},[49,14924,2603],{"class":63},[49,14926,14927],{"class":262}," onAbort",[49,14929,343],{"class":63},[49,14931,4441],{"class":55},[49,14933,648],{"class":63},[49,14935,3862],{"class":55},[49,14937,3865],{"class":262},[49,14939,3477],{"class":55},[49,14941,14942,14945,14947,14949,14952,14955,14957],{"class":51,"line":662},[49,14943,14944],{"class":55},"  a.",[49,14946,11887],{"class":262},[49,14948,625],{"class":55},[49,14950,14951],{"class":70},"'abort'",[49,14953,14954],{"class":55},", onAbort, { once: ",[49,14956,774],{"class":333},[49,14958,1927],{"class":55},[49,14960,14961,14964,14966,14968,14970,14972,14974],{"class":51,"line":691},[49,14962,14963],{"class":55},"  b.",[49,14965,11887],{"class":262},[49,14967,625],{"class":55},[49,14969,14951],{"class":70},[49,14971,14954],{"class":55},[49,14973,774],{"class":333},[49,14975,1927],{"class":55},[49,14977,14978,14980],{"class":51,"line":696},[49,14979,2962],{"class":63},[49,14981,14982],{"class":55}," controller.signal;\n",[49,14984,14985],{"class":51,"line":723},[49,14986,1719],{"class":55},[11,14988,14989,14991,14992,14994,14995,14998,14999,15002],{},[18,14990,14231],{}," تقوم بالدمج أصلياً في المتصفحات الحديثة، أما البديل الاحتياطي فيمرِّر الإشارتين يدوياً إلى متحكِّم جديد. في الحالتين، تصل إشارة واحدة إلى ",[18,14993,13051],{},"، وينتهي انقضاء المهلة أو إلغاء المستدعي إلى مسار الرفض نفسه. وهذا مهم، لأن ",[18,14996,14997],{},"try\u002Fcatch"," يستطيع أن يفحص ",[18,15000,15001],{},"err.name === 'AbortError'"," بغضِّ النظر عن الجهة التي أطلقت الحدث.",[1013,15004,15006],{"id":15005},"الطبقة-الثالثة-interceptors","الطبقة الثالثة: Interceptors",[11,15008,15009],{},"ثلاث سلاسل (طلب، واستجابة، وخطأ) تكفي لتحوِّل عميلاً جامداً إلى عميل قابل للتوسيع:",[40,15011,15013],{"className":1893,"code":15012,"language":1895,"meta":45,"style":45},"type RequestInterceptor = (ctx: { url: string; init: RequestInit }) => void | Promise\u003Cvoid>;\ntype ResponseInterceptor = (res: Response) => Response | Promise\u003CResponse>;\n\nexport function createClient(options: { baseURL: string; defaultHeaders?: Record\u003Cstring, string> }) {\n  const requestInterceptors: RequestInterceptor[] = [];\n  const responseInterceptors: ResponseInterceptor[] = [];\n\n  async function request\u003CT>(path: string, init: RequestInit = {}): Promise\u003CT> {\n    const ctx = {\n      url: new URL(path, options.baseURL).toString(),\n      init: { ...init, headers: { ...(options.defaultHeaders ?? {}), ...(init.headers as Record\u003Cstring, string>) } },\n    };\n    for (const fn of requestInterceptors) await fn(ctx);\n    let res = await fetch(ctx.url, ctx.init);\n    for (const fn of responseInterceptors) res = await fn(res);\n    if (!res.ok) throw new ApiError(res.status, await res.text());\n    return res.json() as Promise\u003CT>;\n  }\n\n  request.useRequest = (fn: RequestInterceptor) => {\n    requestInterceptors.push(fn);\n    return () => void requestInterceptors.splice(requestInterceptors.indexOf(fn), 1);\n  };\n  request.useResponse = (fn: ResponseInterceptor) => {\n    responseInterceptors.push(fn);\n    return () => void responseInterceptors.splice(responseInterceptors.indexOf(fn), 1);\n  };\n\n  return request;\n}\n",[18,15014,15015,15063,15098,15102,15143,15161,15178,15182,15224,15235,15253,15292,15296,15320,15337,15361,15387,15407,15411,15415,15439,15449,15478,15482,15504,15513,15539,15543,15547,15554],{"__ignoreMap":45},[49,15016,15017,15019,15022,15024,15026,15028,15030,15032,15035,15037,15039,15041,15043,15045,15047,15049,15051,15053,15055,15057,15059,15061],{"class":51,"line":52},[49,15018,307],{"class":63},[49,15020,15021],{"class":262}," RequestInterceptor",[49,15023,343],{"class":63},[49,15025,2183],{"class":55},[49,15027,12951],{"class":601},[49,15029,337],{"class":63},[49,15031,701],{"class":55},[49,15033,15034],{"class":601},"url",[49,15036,337],{"class":63},[49,15038,607],{"class":333},[49,15040,3951],{"class":55},[49,15042,14382],{"class":601},[49,15044,337],{"class":63},[49,15046,14387],{"class":262},[49,15048,4494],{"class":55},[49,15050,648],{"class":63},[49,15052,4821],{"class":333},[49,15054,679],{"class":63},[49,15056,3370],{"class":262},[49,15058,56],{"class":55},[49,15060,12326],{"class":333},[49,15062,6834],{"class":55},[49,15064,15065,15067,15070,15072,15074,15077,15079,15082,15084,15086,15088,15090,15092,15094,15096],{"class":51,"line":82},[49,15066,307],{"class":63},[49,15068,15069],{"class":262}," ResponseInterceptor",[49,15071,343],{"class":63},[49,15073,2183],{"class":55},[49,15075,15076],{"class":601},"res",[49,15078,337],{"class":63},[49,15080,15081],{"class":262}," Response",[49,15083,807],{"class":55},[49,15085,648],{"class":63},[49,15087,15081],{"class":262},[49,15089,679],{"class":63},[49,15091,3370],{"class":262},[49,15093,56],{"class":55},[49,15095,14112],{"class":262},[49,15097,6834],{"class":55},[49,15099,15100],{"class":51,"line":88},[49,15101,325],{"emptyLinePlaceholder":324},[49,15103,15104,15106,15108,15110,15112,15114,15116,15118,15120,15122,15124,15126,15128,15130,15132,15134,15136,15138,15140],{"class":51,"line":94},[49,15105,3341],{"class":63},[49,15107,3347],{"class":63},[49,15109,14489],{"class":262},[49,15111,625],{"class":55},[49,15113,5415],{"class":601},[49,15115,337],{"class":63},[49,15117,701],{"class":55},[49,15119,14540],{"class":601},[49,15121,337],{"class":63},[49,15123,607],{"class":333},[49,15125,3951],{"class":55},[49,15127,14545],{"class":601},[49,15129,3946],{"class":63},[49,15131,5455],{"class":262},[49,15133,56],{"class":55},[49,15135,1914],{"class":333},[49,15137,159],{"class":55},[49,15139,1914],{"class":333},[49,15141,15142],{"class":55},"> }) {\n",[49,15144,15145,15147,15150,15152,15154,15157,15159],{"class":51,"line":363},[49,15146,2603],{"class":63},[49,15148,15149],{"class":333}," requestInterceptors",[49,15151,337],{"class":63},[49,15153,15021],{"class":262},[49,15155,15156],{"class":55},"[] ",[49,15158,67],{"class":63},[49,15160,11670],{"class":55},[49,15162,15163,15165,15168,15170,15172,15174,15176],{"class":51,"line":374},[49,15164,2603],{"class":63},[49,15166,15167],{"class":333}," responseInterceptors",[49,15169,337],{"class":63},[49,15171,15069],{"class":262},[49,15173,15156],{"class":55},[49,15175,67],{"class":63},[49,15177,11670],{"class":55},[49,15179,15180],{"class":51,"line":385},[49,15181,325],{"emptyLinePlaceholder":324},[49,15183,15184,15186,15188,15190,15192,15194,15196,15198,15200,15202,15204,15206,15208,15210,15212,15214,15216,15218,15220,15222],{"class":51,"line":662},[49,15185,11641],{"class":63},[49,15187,3347],{"class":63},[49,15189,14364],{"class":262},[49,15191,56],{"class":55},[49,15193,6807],{"class":262},[49,15195,684],{"class":55},[49,15197,14373],{"class":601},[49,15199,337],{"class":63},[49,15201,607],{"class":333},[49,15203,159],{"class":55},[49,15205,14382],{"class":601},[49,15207,337],{"class":63},[49,15209,14387],{"class":262},[49,15211,343],{"class":63},[49,15213,3365],{"class":55},[49,15215,337],{"class":63},[49,15217,3370],{"class":262},[49,15219,56],{"class":55},[49,15221,6807],{"class":262},[49,15223,3377],{"class":55},[49,15225,15226,15228,15231,15233],{"class":51,"line":691},[49,15227,2204],{"class":63},[49,15229,15230],{"class":333}," ctx",[49,15232,343],{"class":63},[49,15234,1625],{"class":55},[49,15236,15237,15240,15243,15245,15248,15250],{"class":51,"line":696},[49,15238,15239],{"class":55},"      url: ",[49,15241,15242],{"class":63},"new",[49,15244,14616],{"class":262},[49,15246,15247],{"class":55},"(path, options.baseURL).",[49,15249,3986],{"class":262},[49,15251,15252],{"class":55},"(),\n",[49,15254,15255,15258,15260,15263,15265,15268,15270,15273,15275,15277,15279,15281,15283,15285,15287,15289],{"class":51,"line":723},[49,15256,15257],{"class":55},"      init: { ",[49,15259,9696],{"class":63},[49,15261,15262],{"class":55},"init, headers: { ",[49,15264,9696],{"class":63},[49,15266,15267],{"class":55},"(options.defaultHeaders ",[49,15269,2555],{"class":63},[49,15271,15272],{"class":55}," {}), ",[49,15274,9696],{"class":63},[49,15276,14644],{"class":55},[49,15278,2657],{"class":63},[49,15280,5455],{"class":262},[49,15282,56],{"class":55},[49,15284,1914],{"class":333},[49,15286,159],{"class":55},[49,15288,1914],{"class":333},[49,15290,15291],{"class":55},">) } },\n",[49,15293,15294],{"class":51,"line":744},[49,15295,6766],{"class":55},[49,15297,15298,15301,15303,15305,15307,15310,15313,15315,15317],{"class":51,"line":768},[49,15299,15300],{"class":63},"    for",[49,15302,2183],{"class":55},[49,15304,330],{"class":63},[49,15306,10745],{"class":333},[49,15308,15309],{"class":63}," of",[49,15311,15312],{"class":55}," requestInterceptors) ",[49,15314,3927],{"class":63},[49,15316,10745],{"class":262},[49,15318,15319],{"class":55},"(ctx);\n",[49,15321,15322,15325,15328,15330,15332,15334],{"class":51,"line":779},[49,15323,15324],{"class":63},"    let",[49,15326,15327],{"class":55}," res ",[49,15329,67],{"class":63},[49,15331,3555],{"class":63},[49,15333,3888],{"class":262},[49,15335,15336],{"class":55},"(ctx.url, ctx.init);\n",[49,15338,15339,15341,15343,15345,15347,15349,15352,15354,15356,15358],{"class":51,"line":784},[49,15340,15300],{"class":63},[49,15342,2183],{"class":55},[49,15344,330],{"class":63},[49,15346,10745],{"class":333},[49,15348,15309],{"class":63},[49,15350,15351],{"class":55}," responseInterceptors) res ",[49,15353,67],{"class":63},[49,15355,3555],{"class":63},[49,15357,10745],{"class":262},[49,15359,15360],{"class":55},"(res);\n",[49,15362,15363,15365,15367,15369,15371,15373,15375,15377,15379,15381,15383,15385],{"class":51,"line":789},[49,15364,2138],{"class":63},[49,15366,2183],{"class":55},[49,15368,2454],{"class":63},[49,15370,3907],{"class":55},[49,15372,14429],{"class":63},[49,15374,3838],{"class":63},[49,15376,13266],{"class":262},[49,15378,14436],{"class":55},[49,15380,3927],{"class":63},[49,15382,3930],{"class":55},[49,15384,13894],{"class":262},[49,15386,14445],{"class":55},[49,15388,15389,15391,15393,15395,15397,15399,15401,15403,15405],{"class":51,"line":820},[49,15390,2945],{"class":63},[49,15392,3930],{"class":55},[49,15394,3933],{"class":262},[49,15396,3615],{"class":55},[49,15398,2657],{"class":63},[49,15400,3370],{"class":262},[49,15402,56],{"class":55},[49,15404,6807],{"class":262},[49,15406,6834],{"class":55},[49,15408,15409],{"class":51,"line":829},[49,15410,2589],{"class":55},[49,15412,15413],{"class":51,"line":834},[49,15414,325],{"emptyLinePlaceholder":324},[49,15416,15417,15420,15422,15424,15426,15429,15431,15433,15435,15437],{"class":51,"line":844},[49,15418,15419],{"class":55},"  request.",[49,15421,12946],{"class":262},[49,15423,343],{"class":63},[49,15425,2183],{"class":55},[49,15427,15428],{"class":601},"fn",[49,15430,337],{"class":63},[49,15432,15021],{"class":262},[49,15434,807],{"class":55},[49,15436,648],{"class":63},[49,15438,1625],{"class":55},[49,15440,15441,15444,15446],{"class":51,"line":861},[49,15442,15443],{"class":55},"    requestInterceptors.",[49,15445,12722],{"class":262},[49,15447,15448],{"class":55},"(fn);\n",[49,15450,15451,15453,15455,15457,15459,15462,15465,15468,15471,15474,15476],{"class":51,"line":888},[49,15452,2945],{"class":63},[49,15454,4441],{"class":55},[49,15456,648],{"class":63},[49,15458,4821],{"class":63},[49,15460,15461],{"class":55}," requestInterceptors.",[49,15463,15464],{"class":262},"splice",[49,15466,15467],{"class":55},"(requestInterceptors.",[49,15469,15470],{"class":262},"indexOf",[49,15472,15473],{"class":55},"(fn), ",[49,15475,2090],{"class":333},[49,15477,630],{"class":55},[49,15479,15480],{"class":51,"line":906},[49,15481,12861],{"class":55},[49,15483,15484,15486,15488,15490,15492,15494,15496,15498,15500,15502],{"class":51,"line":927},[49,15485,15419],{"class":55},[49,15487,12989],{"class":262},[49,15489,343],{"class":63},[49,15491,2183],{"class":55},[49,15493,15428],{"class":601},[49,15495,337],{"class":63},[49,15497,15069],{"class":262},[49,15499,807],{"class":55},[49,15501,648],{"class":63},[49,15503,1625],{"class":55},[49,15505,15506,15509,15511],{"class":51,"line":937},[49,15507,15508],{"class":55},"    responseInterceptors.",[49,15510,12722],{"class":262},[49,15512,15448],{"class":55},[49,15514,15515,15517,15519,15521,15523,15526,15528,15531,15533,15535,15537],{"class":51,"line":2725},[49,15516,2945],{"class":63},[49,15518,4441],{"class":55},[49,15520,648],{"class":63},[49,15522,4821],{"class":63},[49,15524,15525],{"class":55}," responseInterceptors.",[49,15527,15464],{"class":262},[49,15529,15530],{"class":55},"(responseInterceptors.",[49,15532,15470],{"class":262},[49,15534,15473],{"class":55},[49,15536,2090],{"class":333},[49,15538,630],{"class":55},[49,15540,15541],{"class":51,"line":2731},[49,15542,12861],{"class":55},[49,15544,15545],{"class":51,"line":2736},[49,15546,325],{"emptyLinePlaceholder":324},[49,15548,15549,15551],{"class":51,"line":2746},[49,15550,2962],{"class":63},[49,15552,15553],{"class":55}," request;\n",[49,15555,15556],{"class":51,"line":2752},[49,15557,1719],{"class":55},[11,15559,15560,15561,15563],{},"كل تابع ",[18,15562,13040],{}," يُعيد دالة لإلغاء التسجيل، وهذه فائدة عملية حين تسجِّل interceptor داخل مكوِّن وتريد التنظيف عند إزالته.",[11,15565,15566],{},"المصادقة تتحوَّل بذلك إلى سطر واحد:",[40,15568,15570],{"className":1893,"code":15569,"language":1895,"meta":45,"style":45},"api.useRequest((ctx) => {\n  ctx.init.headers = { ...(ctx.init.headers as Record\u003Cstring, string>), Authorization: `Bearer ${token.value}` };\n});\n",[18,15571,15572,15589,15631],{"__ignoreMap":45},[49,15573,15574,15577,15579,15581,15583,15585,15587],{"class":51,"line":52},[49,15575,15576],{"class":55},"api.",[49,15578,12946],{"class":262},[49,15580,801],{"class":55},[49,15582,12951],{"class":601},[49,15584,807],{"class":55},[49,15586,648],{"class":63},[49,15588,1625],{"class":55},[49,15590,15591,15594,15596,15598,15600,15603,15605,15607,15609,15611,15613,15615,15618,15621,15623,15625,15627,15629],{"class":51,"line":82},[49,15592,15593],{"class":55},"  ctx.init.headers ",[49,15595,67],{"class":63},[49,15597,701],{"class":55},[49,15599,9696],{"class":63},[49,15601,15602],{"class":55},"(ctx.init.headers ",[49,15604,2657],{"class":63},[49,15606,5455],{"class":262},[49,15608,56],{"class":55},[49,15610,1914],{"class":333},[49,15612,159],{"class":55},[49,15614,1914],{"class":333},[49,15616,15617],{"class":55},">), Authorization: ",[49,15619,15620],{"class":70},"`Bearer ${",[49,15622,12970],{"class":55},[49,15624,527],{"class":70},[49,15626,4892],{"class":55},[49,15628,739],{"class":70},[49,15630,3960],{"class":55},[49,15632,15633],{"class":51,"line":88},[49,15634,388],{"class":55},[11,15636,15637],{},"وكذلك سيناريو \"تجديد التوكن عند 401\":",[40,15639,15641],{"className":1893,"code":15640,"language":1895,"meta":45,"style":45},"api.useResponse(async (res) => {\n  if (res.status !== 401) return res;\n  await refreshAuth();\n  return fetch(res.url, { \u002F* re-run options *\u002F });\n});\n",[18,15642,15643,15663,15682,15692,15706],{"__ignoreMap":45},[49,15644,15645,15647,15649,15651,15653,15655,15657,15659,15661],{"class":51,"line":52},[49,15646,15576],{"class":55},[49,15648,12989],{"class":262},[49,15650,625],{"class":55},[49,15652,3752],{"class":63},[49,15654,2183],{"class":55},[49,15656,15076],{"class":601},[49,15658,807],{"class":55},[49,15660,648],{"class":63},[49,15662,1625],{"class":55},[49,15664,15665,15667,15670,15672,15675,15677,15679],{"class":51,"line":82},[49,15666,2449],{"class":63},[49,15668,15669],{"class":55}," (res.status ",[49,15671,3815],{"class":63},[49,15673,15674],{"class":333}," 401",[49,15676,807],{"class":55},[49,15678,2197],{"class":63},[49,15680,15681],{"class":55}," res;\n",[49,15683,15684,15687,15690],{"class":51,"line":88},[49,15685,15686],{"class":63},"  await",[49,15688,15689],{"class":262}," refreshAuth",[49,15691,3477],{"class":55},[49,15693,15694,15696,15698,15701,15704],{"class":51,"line":94},[49,15695,2962],{"class":63},[49,15697,3888],{"class":262},[49,15699,15700],{"class":55},"(res.url, { ",[49,15702,15703],{"class":2472},"\u002F* re-run options *\u002F",[49,15705,1927],{"class":55},[49,15707,15708],{"class":51,"line":363},[49,15709,388],{"class":55},[1013,15711,15713],{"id":15712},"الطبقة-الرابعة-إعادة-المحاولة-مع-exponential-backoff","الطبقة الرابعة: إعادة المحاولة مع Exponential Backoff",[11,15715,15716],{},"في الطلبات المُكافئة (GET وHEAD ومعظم PUT وDELETE)، يمتصُّ مساعد retry صغير أغلب الأخطاء الشبكية العابرة:",[40,15718,15720],{"className":1893,"code":15719,"language":1895,"meta":45,"style":45},"async function withRetry\u003CT>(fn: () => Promise\u003CT>, attempts = 2, baseDelayMs = 200): Promise\u003CT> {\n  let lastError: unknown;\n  for (let i = 0; i \u003C= attempts; i++) {\n    try {\n      return await fn();\n    } catch (e) {\n      lastError = e;\n      if (i \u003C attempts) await new Promise((r) => setTimeout(r, baseDelayMs * 2 ** i));\n    }\n  }\n  throw lastError;\n}\n",[18,15721,15722,15781,15795,15824,15830,15840,15849,15858,15900,15904,15908,15916],{"__ignoreMap":45},[49,15723,15724,15726,15728,15731,15733,15735,15737,15739,15741,15743,15745,15747,15749,15751,15753,15755,15757,15760,15762,15764,15766,15769,15771,15773,15775,15777,15779],{"class":51,"line":52},[49,15725,3752],{"class":63},[49,15727,3347],{"class":63},[49,15729,15730],{"class":262}," withRetry",[49,15732,56],{"class":55},[49,15734,6807],{"class":262},[49,15736,684],{"class":55},[49,15738,15428],{"class":262},[49,15740,337],{"class":63},[49,15742,4441],{"class":55},[49,15744,648],{"class":63},[49,15746,3370],{"class":262},[49,15748,56],{"class":55},[49,15750,6807],{"class":262},[49,15752,7962],{"class":55},[49,15754,12841],{"class":601},[49,15756,343],{"class":63},[49,15758,15759],{"class":333}," 2",[49,15761,159],{"class":55},[49,15763,12850],{"class":601},[49,15765,343],{"class":63},[49,15767,15768],{"class":333}," 200",[49,15770,2433],{"class":55},[49,15772,337],{"class":63},[49,15774,3370],{"class":262},[49,15776,56],{"class":55},[49,15778,6807],{"class":262},[49,15780,3377],{"class":55},[49,15782,15783,15786,15789,15791,15793],{"class":51,"line":82},[49,15784,15785],{"class":63},"  let",[49,15787,15788],{"class":55}," lastError",[49,15790,337],{"class":63},[49,15792,4446],{"class":333},[49,15794,319],{"class":55},[49,15796,15797,15799,15801,15803,15806,15808,15810,15813,15816,15819,15822],{"class":51,"line":88},[49,15798,2776],{"class":63},[49,15800,2183],{"class":55},[49,15802,2781],{"class":63},[49,15804,15805],{"class":55}," i ",[49,15807,67],{"class":63},[49,15809,8943],{"class":333},[49,15811,15812],{"class":55},"; i ",[49,15814,15815],{"class":63},"\u003C=",[49,15817,15818],{"class":55}," attempts; i",[49,15820,15821],{"class":63},"++",[49,15823,2149],{"class":55},[49,15825,15826,15828],{"class":51,"line":94},[49,15827,2639],{"class":63},[49,15829,1625],{"class":55},[49,15831,15832,15834,15836,15838],{"class":51,"line":363},[49,15833,2169],{"class":63},[49,15835,3555],{"class":63},[49,15837,10745],{"class":262},[49,15839,3477],{"class":55},[49,15841,15842,15844,15846],{"class":51,"line":374},[49,15843,2739],{"class":55},[49,15845,2577],{"class":63},[49,15847,15848],{"class":55}," (e) {\n",[49,15850,15851,15854,15856],{"class":51,"line":385},[49,15852,15853],{"class":55},"      lastError ",[49,15855,67],{"class":63},[49,15857,13356],{"class":55},[49,15859,15860,15862,15865,15867,15870,15872,15874,15876,15878,15881,15883,15885,15887,15890,15892,15894,15897],{"class":51,"line":662},[49,15861,2154],{"class":63},[49,15863,15864],{"class":55}," (i ",[49,15866,56],{"class":63},[49,15868,15869],{"class":55}," attempts) ",[49,15871,3927],{"class":63},[49,15873,3838],{"class":63},[49,15875,3370],{"class":333},[49,15877,801],{"class":55},[49,15879,15880],{"class":601},"r",[49,15882,807],{"class":55},[49,15884,648],{"class":63},[49,15886,3855],{"class":262},[49,15888,15889],{"class":55},"(r, baseDelayMs ",[49,15891,13483],{"class":63},[49,15893,15759],{"class":333},[49,15895,15896],{"class":63}," **",[49,15898,15899],{"class":55}," i));\n",[49,15901,15902],{"class":51,"line":691},[49,15903,2176],{"class":55},[49,15905,15906],{"class":51,"line":696},[49,15907,2589],{"class":55},[49,15909,15910,15913],{"class":51,"line":723},[49,15911,15912],{"class":63},"  throw",[49,15914,15915],{"class":55}," lastError;\n",[49,15917,15918],{"class":51,"line":744},[49,15919,1719],{"class":55},[11,15921,15922,15923,1251],{},"ثلاث محاولات مع تأخير 200ms ثم 400ms ثم 800ms تُغطِّي معظم أخطاء 502 العابرة وتقطُّعات DNS. أما إعادة محاولة الطلبات غير المُكافئة فعلى مسؤوليتك، لأن إعادة إرسال POST ثبت في قاعدة البيانات فعلاً أسوأ بكثير من الفشل بوضوح. وأسهل طريقة لضبط ذلك هي قائمة سماح صغيرة داخل الغلاف نفسه (",[18,15924,15925],{},"['GET', 'HEAD', 'PUT', 'DELETE']",[1013,15927,15929],{"id":15928},"النتيجة-النهائية","النتيجة النهائية",[11,15931,15932],{},"بعد الطبقات الأربع، تصبح واجهة الاستهلاك بهذا الشكل:",[40,15934,15936],{"className":1893,"code":15935,"language":1895,"meta":45,"style":45},"const api = createClient({\n  baseURL: 'https:\u002F\u002Fapi.example.com',\n  defaultHeaders: { Accept: 'application\u002Fjson' },\n});\n\napi.useRequest((ctx) => {\n  ctx.init.headers = { ...(ctx.init.headers as Record\u003Cstring, string>), Authorization: `Bearer ${token.value}` };\n});\n\nconst users = await api\u003CUser[]>('\u002Fusers');\n",[18,15937,15938,15951,15960,15970,15974,15978,15994,16032,16036,16040],{"__ignoreMap":45},[49,15939,15940,15942,15945,15947,15949],{"class":51,"line":52},[49,15941,330],{"class":63},[49,15943,15944],{"class":333}," api",[49,15946,343],{"class":63},[49,15948,14489],{"class":262},[49,15950,720],{"class":55},[49,15952,15953,15956,15958],{"class":51,"line":82},[49,15954,15955],{"class":55},"  baseURL: ",[49,15957,13132],{"class":70},[49,15959,360],{"class":55},[49,15961,15962,15965,15968],{"class":51,"line":88},[49,15963,15964],{"class":55},"  defaultHeaders: { Accept: ",[49,15966,15967],{"class":70},"'application\u002Fjson'",[49,15969,7633],{"class":55},[49,15971,15972],{"class":51,"line":94},[49,15973,388],{"class":55},[49,15975,15976],{"class":51,"line":363},[49,15977,325],{"emptyLinePlaceholder":324},[49,15979,15980,15982,15984,15986,15988,15990,15992],{"class":51,"line":374},[49,15981,15576],{"class":55},[49,15983,12946],{"class":262},[49,15985,801],{"class":55},[49,15987,12951],{"class":601},[49,15989,807],{"class":55},[49,15991,648],{"class":63},[49,15993,1625],{"class":55},[49,15995,15996,15998,16000,16002,16004,16006,16008,16010,16012,16014,16016,16018,16020,16022,16024,16026,16028,16030],{"class":51,"line":385},[49,15997,15593],{"class":55},[49,15999,67],{"class":63},[49,16001,701],{"class":55},[49,16003,9696],{"class":63},[49,16005,15602],{"class":55},[49,16007,2657],{"class":63},[49,16009,5455],{"class":262},[49,16011,56],{"class":55},[49,16013,1914],{"class":333},[49,16015,159],{"class":55},[49,16017,1914],{"class":333},[49,16019,15617],{"class":55},[49,16021,15620],{"class":70},[49,16023,12970],{"class":55},[49,16025,527],{"class":70},[49,16027,4892],{"class":55},[49,16029,739],{"class":70},[49,16031,3960],{"class":55},[49,16033,16034],{"class":51,"line":662},[49,16035,388],{"class":55},[49,16037,16038],{"class":51,"line":691},[49,16039,325],{"emptyLinePlaceholder":324},[49,16041,16042,16044,16046,16048,16050,16052,16054,16056,16058,16060],{"class":51,"line":696},[49,16043,330],{"class":63},[49,16045,12904],{"class":333},[49,16047,343],{"class":63},[49,16049,3555],{"class":63},[49,16051,15944],{"class":262},[49,16053,56],{"class":55},[49,16055,12916],{"class":262},[49,16057,12919],{"class":55},[49,16059,12922],{"class":70},[49,16061,630],{"class":55},[11,16063,16064,16065,527],{},"استدعاء مسطَّح، ومصادقة تُدار مرَّة واحدة، وأخطاء تُرمى بنوع معروف، ومهلات وإعادة محاولة تُطبَّق حيث ينبغي. كل طبقة دالة عادية تلفُّ التي تحتها، وفي القاع لا يزال ",[18,16066,13051],{},[11,16068,16069],{},"وهنا بالضبط تظهر ثغرة تقدُّم الرفع.",[26,16071,16073,16074,16076],{"id":16072},"لماذا-يعجز-fetch-عن-إظهار-تقدُّم-الرفع","لماذا يعجز ",[18,16075,13051],{}," عن إظهار تقدُّم الرفع",[11,16078,16079,16080,16082],{},"تقدُّم التنزيل مع ",[18,16081,13051],{}," يعمل، لكن بطريقة فيها بعض الحرج. تقرأ جسم الاستجابة كـ stream:",[40,16084,16086],{"className":1893,"code":16085,"language":1895,"meta":45,"style":45},"const res = await fetch('\u002Fbig.zip');\nconst total = Number(res.headers.get('content-length')) || null;\nlet loaded = 0;\nconst reader = res.body!.getReader();\nwhile (true) {\n  const { value, done } = await reader.read();\n  if (done) break;\n  loaded += value.byteLength;\n  onProgress(loaded, total);\n}\n",[18,16087,16088,16107,16137,16150,16171,16182,16209,16221,16232,16240],{"__ignoreMap":45},[49,16089,16090,16092,16094,16096,16098,16100,16102,16105],{"class":51,"line":52},[49,16091,330],{"class":63},[49,16093,3881],{"class":333},[49,16095,343],{"class":63},[49,16097,3555],{"class":63},[49,16099,3888],{"class":262},[49,16101,625],{"class":55},[49,16103,16104],{"class":70},"'\u002Fbig.zip'",[49,16106,630],{"class":55},[49,16108,16109,16111,16114,16116,16118,16121,16123,16125,16128,16131,16133,16135],{"class":51,"line":82},[49,16110,330],{"class":63},[49,16112,16113],{"class":333}," total",[49,16115,343],{"class":63},[49,16117,6138],{"class":262},[49,16119,16120],{"class":55},"(res.headers.",[49,16122,13661],{"class":262},[49,16124,625],{"class":55},[49,16126,16127],{"class":70},"'content-length'",[49,16129,16130],{"class":55},")) ",[49,16132,2693],{"class":63},[49,16134,657],{"class":333},[49,16136,319],{"class":55},[49,16138,16139,16141,16144,16146,16148],{"class":51,"line":88},[49,16140,2781],{"class":63},[49,16142,16143],{"class":55}," loaded ",[49,16145,67],{"class":63},[49,16147,8943],{"class":333},[49,16149,319],{"class":55},[49,16151,16152,16154,16157,16159,16162,16164,16166,16169],{"class":51,"line":94},[49,16153,330],{"class":63},[49,16155,16156],{"class":333}," reader",[49,16158,343],{"class":63},[49,16160,16161],{"class":55}," res.body",[49,16163,2454],{"class":63},[49,16165,527],{"class":55},[49,16167,16168],{"class":262},"getReader",[49,16170,3477],{"class":55},[49,16172,16173,16176,16178,16180],{"class":51,"line":363},[49,16174,16175],{"class":63},"while",[49,16177,2183],{"class":55},[49,16179,774],{"class":333},[49,16181,2149],{"class":55},[49,16183,16184,16186,16188,16190,16192,16195,16197,16199,16201,16204,16207],{"class":51,"line":374},[49,16185,2603],{"class":63},[49,16187,701],{"class":55},[49,16189,4892],{"class":333},[49,16191,159],{"class":55},[49,16193,16194],{"class":333},"done",[49,16196,712],{"class":55},[49,16198,67],{"class":63},[49,16200,3555],{"class":63},[49,16202,16203],{"class":55}," reader.",[49,16205,16206],{"class":262},"read",[49,16208,3477],{"class":55},[49,16210,16211,16213,16216,16219],{"class":51,"line":385},[49,16212,2449],{"class":63},[49,16214,16215],{"class":55}," (done) ",[49,16217,16218],{"class":63},"break",[49,16220,319],{"class":55},[49,16222,16223,16226,16229],{"class":51,"line":662},[49,16224,16225],{"class":55},"  loaded ",[49,16227,16228],{"class":63},"+=",[49,16230,16231],{"class":55}," value.byteLength;\n",[49,16233,16234,16237],{"class":51,"line":691},[49,16235,16236],{"class":262},"  onProgress",[49,16238,16239],{"class":55},"(loaded, total);\n",[49,16241,16242],{"class":51,"line":696},[49,16243,1719],{"class":55},[11,16245,16246,16247,16249],{},"هذا مقبول. صحيح أن عليك أن تحتفظ بالقطع بنفسك (أو أن تدفعها إلى ",[18,16248,14112],{}," جديد إن أردتَ تمريرها إلى شيء آخر)، لكن البدائيات موجودة على الأقل.",[11,16251,16252,16253,16255,16256,16258,16259,16262,16263,16265,16266,16269],{},"أما في الرفع، فالقطعة المفقودة أن جسم ",[18,16254,14109],{}," تستهلكه شبكة المتصفح، لا شيفرتك. تستطيع أن تمرِّر ",[18,16257,13058],{},"، لكن لا يوجد أي حدث يُطلَق أثناء استنزاف المتصفح له. بعض إصدارات Chromium تدعم ",[18,16260,16261],{},"Request.duplex: 'half'"," مما يفتح باب streaming الرفع، غير أن حدث ",[18,16264,13062],{}," لا يزال غائباً. سيتوجَّب عليك أن تُجهِّز ",[18,16267,16268],{},"pull()"," الخاص بالـ stream يدوياً لعدِّ البايتات، وحتى إذا فعلتَ ذلك فأنت تقيس ما يُطلقه الـ stream، لا ما أرسله الـ socket فعلاً.",[11,16271,16272],{},"عملياً، إن أردتَ شريط تقدُّم رفع في سنة 2026، فستستخدم XHR.",[26,16274,16276],{"id":16275},"الأحداث-الخمسة-في-xhr-التي-تهمُّك-حقاً","الأحداث الخمسة في XHR التي تهمُّك حقاً",[11,16278,16279],{},"يُطلق كل XHR مجموعة أحداث ثابتة ومتوقَّعة. والذي يهم منها لواجهات التقدُّم هو:",[40,16281,16283],{"className":1893,"code":16282,"language":1895,"meta":45,"style":45},"xhr.upload.onprogress = (e: ProgressEvent) => { \u002F* upload bytes *\u002F };\nxhr.onprogress        = (e: ProgressEvent) => { \u002F* download bytes *\u002F };\nxhr.onload            = () => { \u002F* request completed successfully *\u002F };\nxhr.onerror           = () => { \u002F* network error *\u002F };\nxhr.onabort           = () => { \u002F* xhr.abort() was called *\u002F };\nxhr.ontimeout         = () => { \u002F* xhr.timeout exceeded *\u002F };\n",[18,16284,16285,16315,16344,16365,16386,16406],{"__ignoreMap":45},[49,16286,16287,16290,16293,16295,16297,16299,16301,16304,16306,16308,16310,16313],{"class":51,"line":52},[49,16288,16289],{"class":55},"xhr.upload.",[49,16291,16292],{"class":262},"onprogress",[49,16294,343],{"class":63},[49,16296,2183],{"class":55},[49,16298,13309],{"class":601},[49,16300,337],{"class":63},[49,16302,16303],{"class":262}," ProgressEvent",[49,16305,807],{"class":55},[49,16307,648],{"class":63},[49,16309,701],{"class":55},[49,16311,16312],{"class":2472},"\u002F* upload bytes *\u002F",[49,16314,3960],{"class":55},[49,16316,16317,16320,16322,16325,16327,16329,16331,16333,16335,16337,16339,16342],{"class":51,"line":82},[49,16318,16319],{"class":55},"xhr.",[49,16321,16292],{"class":262},[49,16323,16324],{"class":63},"        =",[49,16326,2183],{"class":55},[49,16328,13309],{"class":601},[49,16330,337],{"class":63},[49,16332,16303],{"class":262},[49,16334,807],{"class":55},[49,16336,648],{"class":63},[49,16338,701],{"class":55},[49,16340,16341],{"class":2472},"\u002F* download bytes *\u002F",[49,16343,3960],{"class":55},[49,16345,16346,16348,16351,16354,16356,16358,16360,16363],{"class":51,"line":88},[49,16347,16319],{"class":55},[49,16349,16350],{"class":262},"onload",[49,16352,16353],{"class":63},"            =",[49,16355,4441],{"class":55},[49,16357,648],{"class":63},[49,16359,701],{"class":55},[49,16361,16362],{"class":2472},"\u002F* request completed successfully *\u002F",[49,16364,3960],{"class":55},[49,16366,16367,16369,16372,16375,16377,16379,16381,16384],{"class":51,"line":94},[49,16368,16319],{"class":55},[49,16370,16371],{"class":262},"onerror",[49,16373,16374],{"class":63},"           =",[49,16376,4441],{"class":55},[49,16378,648],{"class":63},[49,16380,701],{"class":55},[49,16382,16383],{"class":2472},"\u002F* network error *\u002F",[49,16385,3960],{"class":55},[49,16387,16388,16390,16393,16395,16397,16399,16401,16404],{"class":51,"line":363},[49,16389,16319],{"class":55},[49,16391,16392],{"class":262},"onabort",[49,16394,16374],{"class":63},[49,16396,4441],{"class":55},[49,16398,648],{"class":63},[49,16400,701],{"class":55},[49,16402,16403],{"class":2472},"\u002F* xhr.abort() was called *\u002F",[49,16405,3960],{"class":55},[49,16407,16408,16410,16413,16416,16418,16420,16422,16425],{"class":51,"line":374},[49,16409,16319],{"class":55},[49,16411,16412],{"class":262},"ontimeout",[49,16414,16415],{"class":63},"         =",[49,16417,4441],{"class":55},[49,16419,648],{"class":63},[49,16421,701],{"class":55},[49,16423,16424],{"class":2472},"\u002F* xhr.timeout exceeded *\u002F",[49,16426,3960],{"class":55},[11,16428,16429],{},"ثمَّة أمران يسهل أن يفوتك الانتباه إليهما.",[11,16431,16432,16433,16436,16437,16440,16441,16443,16444,16447,16448,16450],{},"الأوَّل أن ",[18,16434,16435],{},"xhr.upload"," كائن مستقل من نوع ",[18,16438,16439],{},"XMLHttpRequestUpload","، وله هدف أحداث خاص به. هو الذي يُطلق ",[18,16442,13062],{}," أثناء إرسال جسم الطلب، بينما ",[18,16445,16446],{},"xhr"," نفسه يُطلق ",[18,16449,13062],{}," أثناء استقبال جسم الاستجابة. إنهما مرحلتان في الطلب نفسه، ويشتركان في شكل الحدث ذاته.",[11,16452,16453,16454,16457,16458,16461,16462,527],{},"الثاني أن ",[18,16455,16456],{},"xhr.onload"," يُطلَق مع أي طلب مكتمل، حتى لو أرجع HTTP 500. معنى \"اكتمل الطلب\" هنا هو \"ردَّ الخادم\"، لا أكثر. فإن أردتَ التحقُّق من أن \"الطلب نجح\" فعلاً، فافحص ",[18,16459,16460],{},"xhr.status"," بنفسك داخل ",[18,16463,16350],{},[11,16465,16466,16467,394,16470,394,16473,16476],{},"هناك أحداث أخرى (",[18,16468,16469],{},"loadstart",[18,16471,16472],{},"loadend",[18,16474,16475],{},"readystatechange",")، لكن الأحداث الخمسة أعلاه تكفي واجهة التقدُّم تماماً.",[26,16478,16480,16481,16484,16485],{"id":16479},"شكل-progressevent-وحكاية-lengthcomputable","شكل ",[18,16482,16483],{},"ProgressEvent","، وحكاية ",[18,16486,16487],{},"lengthComputable",[11,16489,16490,16491,16493,16494,16496],{},"يستقبل كلا الـ callback في ",[18,16492,16292],{}," كائن ",[18,16495,16483],{}," يحوي ثلاثة حقول:",[40,16498,16500],{"className":1893,"code":16499,"language":1895,"meta":45,"style":45},"interface ProgressEvent {\n  lengthComputable: boolean;\n  loaded: number;   \u002F\u002F bytes transferred so far\n  total: number;    \u002F\u002F bytes expected, ONLY valid when lengthComputable is true\n}\n",[18,16501,16502,16511,16522,16537,16552],{"__ignoreMap":45},[49,16503,16504,16507,16509],{"class":51,"line":52},[49,16505,16506],{"class":63},"interface",[49,16508,16303],{"class":262},[49,16510,1625],{"class":55},[49,16512,16513,16516,16518,16520],{"class":51,"line":82},[49,16514,16515],{"class":601},"  lengthComputable",[49,16517,337],{"class":63},[49,16519,4678],{"class":333},[49,16521,319],{"class":55},[49,16523,16524,16527,16529,16531,16534],{"class":51,"line":88},[49,16525,16526],{"class":601},"  loaded",[49,16528,337],{"class":63},[49,16530,3775],{"class":333},[49,16532,16533],{"class":55},";   ",[49,16535,16536],{"class":2472},"\u002F\u002F bytes transferred so far\n",[49,16538,16539,16542,16544,16546,16549],{"class":51,"line":94},[49,16540,16541],{"class":601},"  total",[49,16543,337],{"class":63},[49,16545,3775],{"class":333},[49,16547,16548],{"class":55},";    ",[49,16550,16551],{"class":2472},"\u002F\u002F bytes expected, ONLY valid when lengthComputable is true\n",[49,16553,16554],{"class":51,"line":363},[49,16555,1719],{"class":55},[11,16557,16558,16560,16561,16563,16564,16567,16568,16570,16571,16574,16575,16577,16578,16580,16581,3728,16583,527],{},[18,16559,16487],{}," هو الحقل الذي يتجاهله أكثر من ينفِّذ الأمر لأول مرَّة. تصبح قيمته ",[18,16562,11087],{}," حين يُرسل الخادم استجابة مُقطَّعة دون ترويسة ",[18,16565,16566],{},"Content-Length","، وتصبح ",[18,16569,11087],{}," كذلك أثناء عمليات الرفع التي يكون فيها الجسم stream مجهول الطول. وفي هذه الحالة تكون ",[18,16572,16573],{},"total"," تساوي ",[18,16576,2839],{}," بالضبط، لا \"غير موجودة\" ولا ",[18,16579,7334],{},"، بل الرقم صفر، وحين تقسم عليه في شريط التقدُّم يخرج لك ",[18,16582,12308],{},[18,16584,12311],{},[11,16586,16587],{},"لذلك ضع التطبيع في مكان واحد:",[40,16589,16591],{"className":1893,"code":16590,"language":1895,"meta":45,"style":45},"function normaliseProgress(phase: 'upload' | 'download', e: ProgressEvent) {\n  const total = e.lengthComputable ? e.total : null;\n  const ratio = total && total > 0 ? e.loaded \u002F total : null;\n  return { phase, loaded: e.loaded, total, ratio };\n}\n",[18,16592,16593,16625,16647,16682,16689],{"__ignoreMap":45},[49,16594,16595,16597,16600,16602,16605,16607,16610,16612,16615,16617,16619,16621,16623],{"class":51,"line":52},[49,16596,2418],{"class":63},[49,16598,16599],{"class":262}," normaliseProgress",[49,16601,625],{"class":55},[49,16603,16604],{"class":601},"phase",[49,16606,337],{"class":63},[49,16608,16609],{"class":70}," 'upload'",[49,16611,679],{"class":63},[49,16613,16614],{"class":70}," 'download'",[49,16616,159],{"class":55},[49,16618,13309],{"class":601},[49,16620,337],{"class":63},[49,16622,16303],{"class":262},[49,16624,2149],{"class":55},[49,16626,16627,16629,16631,16633,16636,16638,16641,16643,16645],{"class":51,"line":82},[49,16628,2603],{"class":63},[49,16630,16113],{"class":333},[49,16632,343],{"class":63},[49,16634,16635],{"class":55}," e.lengthComputable ",[49,16637,758],{"class":63},[49,16639,16640],{"class":55}," e.total ",[49,16642,337],{"class":63},[49,16644,657],{"class":333},[49,16646,319],{"class":55},[49,16648,16649,16651,16654,16656,16659,16661,16663,16665,16667,16669,16672,16674,16676,16678,16680],{"class":51,"line":88},[49,16650,2603],{"class":63},[49,16652,16653],{"class":333}," ratio",[49,16655,343],{"class":63},[49,16657,16658],{"class":55}," total ",[49,16660,2513],{"class":63},[49,16662,16658],{"class":55},[49,16664,5443],{"class":63},[49,16666,8943],{"class":333},[49,16668,6723],{"class":63},[49,16670,16671],{"class":55}," e.loaded ",[49,16673,4011],{"class":63},[49,16675,16658],{"class":55},[49,16677,337],{"class":63},[49,16679,657],{"class":333},[49,16681,319],{"class":55},[49,16683,16684,16686],{"class":51,"line":94},[49,16685,2962],{"class":63},[49,16687,16688],{"class":55}," { phase, loaded: e.loaded, total, ratio };\n",[49,16690,16691],{"class":51,"line":363},[49,16692,1719],{"class":55},[11,16694,16695,16697,16698,16700],{},[18,16696,172],{}," هو الجواب الأمين حين لا تعرف المجموع، وطبقة الواجهة هي التي تقرِّر ما تعرضه: عدَّاد بايتات بلا نسبة مئوية، أو شريط غير محدَّد، أو مؤشِّر دوران. المهم ألا تسمح بأن يتسرَّب ",[18,16699,12308],{}," من طبقة النقل.",[26,16702,16704],{"id":16703},"أبسط-مثال-لتقدُّم-الرفع","أبسط مثال لتقدُّم الرفع",[11,16706,16707],{},"هذا أصغر مثال مفيد لرفع مع تقدُّم، يُظهر واجهة الاستخدام دون أي غلاف:",[40,16709,16711],{"className":1893,"code":16710,"language":1895,"meta":45,"style":45},"async function uploadWithProgress(\n  file: File,\n  onProgress: (loaded: number, total: number | null) => void\n): Promise\u003Cvoid> {\n  await new Promise\u003Cvoid>((resolve, reject) => {\n    const xhr = new XMLHttpRequest();\n    xhr.open('POST', '\u002Fupload', true);\n\n    xhr.upload.onprogress = (e) => {\n      const total = e.lengthComputable ? e.total : null;\n      onProgress(e.loaded, total);\n    };\n\n    xhr.onload = () => {\n      if (xhr.status >= 200 && xhr.status \u003C 300) resolve();\n      else reject(new Error(`HTTP ${xhr.status}`));\n    };\n    xhr.onerror = () => reject(new TypeError('Network error'));\n    xhr.onabort = () => reject(new DOMException('Aborted', 'AbortError'));\n\n    const form = new FormData();\n    form.append('file', file);\n    xhr.send(form);\n  });\n}\n",[18,16712,16713,16724,16736,16770,16784,16812,16828,16851,16855,16874,16894,16902,16906,16910,16924,16951,16982,16986,17014,17047,17051,17067,17083,17093,17097],{"__ignoreMap":45},[49,16714,16715,16717,16719,16722],{"class":51,"line":52},[49,16716,3752],{"class":63},[49,16718,3347],{"class":63},[49,16720,16721],{"class":262}," uploadWithProgress",[49,16723,2114],{"class":55},[49,16725,16726,16729,16731,16734],{"class":51,"line":82},[49,16727,16728],{"class":601},"  file",[49,16730,337],{"class":63},[49,16732,16733],{"class":262}," File",[49,16735,360],{"class":55},[49,16737,16738,16740,16742,16744,16747,16749,16751,16753,16755,16757,16759,16761,16763,16765,16767],{"class":51,"line":88},[49,16739,16236],{"class":262},[49,16741,337],{"class":63},[49,16743,2183],{"class":55},[49,16745,16746],{"class":601},"loaded",[49,16748,337],{"class":63},[49,16750,3775],{"class":333},[49,16752,159],{"class":55},[49,16754,16573],{"class":601},[49,16756,337],{"class":63},[49,16758,3775],{"class":333},[49,16760,679],{"class":63},[49,16762,657],{"class":333},[49,16764,807],{"class":55},[49,16766,648],{"class":63},[49,16768,16769],{"class":333}," void\n",[49,16771,16772,16774,16776,16778,16780,16782],{"class":51,"line":94},[49,16773,2433],{"class":55},[49,16775,337],{"class":63},[49,16777,3370],{"class":262},[49,16779,56],{"class":55},[49,16781,12326],{"class":333},[49,16783,3377],{"class":55},[49,16785,16786,16788,16790,16792,16794,16796,16799,16801,16803,16806,16808,16810],{"class":51,"line":363},[49,16787,15686],{"class":63},[49,16789,3838],{"class":63},[49,16791,3370],{"class":333},[49,16793,56],{"class":55},[49,16795,12326],{"class":333},[49,16797,16798],{"class":55},">((",[49,16800,4238],{"class":601},[49,16802,159],{"class":55},[49,16804,16805],{"class":601},"reject",[49,16807,807],{"class":55},[49,16809,648],{"class":63},[49,16811,1625],{"class":55},[49,16813,16814,16816,16819,16821,16823,16826],{"class":51,"line":374},[49,16815,2204],{"class":63},[49,16817,16818],{"class":333}," xhr",[49,16820,343],{"class":63},[49,16822,3838],{"class":63},[49,16824,16825],{"class":262}," XMLHttpRequest",[49,16827,3477],{"class":55},[49,16829,16830,16833,16835,16837,16840,16842,16845,16847,16849],{"class":51,"line":385},[49,16831,16832],{"class":55},"    xhr.",[49,16834,4291],{"class":262},[49,16836,625],{"class":55},[49,16838,16839],{"class":70},"'POST'",[49,16841,159],{"class":55},[49,16843,16844],{"class":70},"'\u002Fupload'",[49,16846,159],{"class":55},[49,16848,774],{"class":333},[49,16850,630],{"class":55},[49,16852,16853],{"class":51,"line":662},[49,16854,325],{"emptyLinePlaceholder":324},[49,16856,16857,16860,16862,16864,16866,16868,16870,16872],{"class":51,"line":691},[49,16858,16859],{"class":55},"    xhr.upload.",[49,16861,16292],{"class":262},[49,16863,343],{"class":63},[49,16865,2183],{"class":55},[49,16867,13309],{"class":601},[49,16869,807],{"class":55},[49,16871,648],{"class":63},[49,16873,1625],{"class":55},[49,16875,16876,16878,16880,16882,16884,16886,16888,16890,16892],{"class":51,"line":696},[49,16877,2521],{"class":63},[49,16879,16113],{"class":333},[49,16881,343],{"class":63},[49,16883,16635],{"class":55},[49,16885,758],{"class":63},[49,16887,16640],{"class":55},[49,16889,337],{"class":63},[49,16891,657],{"class":333},[49,16893,319],{"class":55},[49,16895,16896,16899],{"class":51,"line":723},[49,16897,16898],{"class":262},"      onProgress",[49,16900,16901],{"class":55},"(e.loaded, total);\n",[49,16903,16904],{"class":51,"line":744},[49,16905,6766],{"class":55},[49,16907,16908],{"class":51,"line":768},[49,16909,325],{"emptyLinePlaceholder":324},[49,16911,16912,16914,16916,16918,16920,16922],{"class":51,"line":779},[49,16913,16832],{"class":55},[49,16915,16350],{"class":262},[49,16917,343],{"class":63},[49,16919,4441],{"class":55},[49,16921,648],{"class":63},[49,16923,1625],{"class":55},[49,16925,16926,16928,16931,16933,16935,16937,16940,16942,16945,16947,16949],{"class":51,"line":784},[49,16927,2154],{"class":63},[49,16929,16930],{"class":55}," (xhr.status ",[49,16932,2808],{"class":63},[49,16934,15768],{"class":333},[49,16936,8933],{"class":63},[49,16938,16939],{"class":55}," xhr.status ",[49,16941,56],{"class":63},[49,16943,16944],{"class":333}," 300",[49,16946,807],{"class":55},[49,16948,4238],{"class":262},[49,16950,3477],{"class":55},[49,16952,16953,16956,16959,16961,16963,16965,16967,16970,16972,16974,16977,16979],{"class":51,"line":789},[49,16954,16955],{"class":63},"      else",[49,16957,16958],{"class":262}," reject",[49,16960,625],{"class":55},[49,16962,15242],{"class":63},[49,16964,13272],{"class":262},[49,16966,625],{"class":55},[49,16968,16969],{"class":70},"`HTTP ${",[49,16971,16446],{"class":55},[49,16973,527],{"class":70},[49,16975,16976],{"class":55},"status",[49,16978,739],{"class":70},[49,16980,16981],{"class":55},"));\n",[49,16983,16984],{"class":51,"line":820},[49,16985,6766],{"class":55},[49,16987,16988,16990,16992,16994,16996,16998,17000,17002,17004,17007,17009,17012],{"class":51,"line":829},[49,16989,16832],{"class":55},[49,16991,16371],{"class":262},[49,16993,343],{"class":63},[49,16995,4441],{"class":55},[49,16997,648],{"class":63},[49,16999,16958],{"class":262},[49,17001,625],{"class":55},[49,17003,15242],{"class":63},[49,17005,17006],{"class":262}," TypeError",[49,17008,625],{"class":55},[49,17010,17011],{"class":70},"'Network error'",[49,17013,16981],{"class":55},[49,17015,17016,17018,17020,17022,17024,17026,17028,17030,17032,17035,17037,17040,17042,17045],{"class":51,"line":834},[49,17017,16832],{"class":55},[49,17019,16392],{"class":262},[49,17021,343],{"class":63},[49,17023,4441],{"class":55},[49,17025,648],{"class":63},[49,17027,16958],{"class":262},[49,17029,625],{"class":55},[49,17031,15242],{"class":63},[49,17033,17034],{"class":262}," DOMException",[49,17036,625],{"class":55},[49,17038,17039],{"class":70},"'Aborted'",[49,17041,159],{"class":55},[49,17043,17044],{"class":70},"'AbortError'",[49,17046,16981],{"class":55},[49,17048,17049],{"class":51,"line":844},[49,17050,325],{"emptyLinePlaceholder":324},[49,17052,17053,17055,17058,17060,17062,17065],{"class":51,"line":861},[49,17054,2204],{"class":63},[49,17056,17057],{"class":333}," form",[49,17059,343],{"class":63},[49,17061,3838],{"class":63},[49,17063,17064],{"class":262}," FormData",[49,17066,3477],{"class":55},[49,17068,17069,17072,17075,17077,17080],{"class":51,"line":888},[49,17070,17071],{"class":55},"    form.",[49,17073,17074],{"class":262},"append",[49,17076,625],{"class":55},[49,17078,17079],{"class":70},"'file'",[49,17081,17082],{"class":55},", file);\n",[49,17084,17085,17087,17090],{"class":51,"line":906},[49,17086,16832],{"class":55},[49,17088,17089],{"class":262},"send",[49,17091,17092],{"class":55},"(form);\n",[49,17094,17095],{"class":51,"line":927},[49,17096,8036],{"class":55},[49,17098,17099],{"class":51,"line":937},[49,17100,1719],{"class":55},[11,17102,17103],{},"ثلاث ملاحظات تستحق التوقُّف عندها:",[8674,17105,17106,17123,17134],{},[3673,17107,17108,17118,17119,17122],{},[36,17109,17110,17111,17114,17115,527],{},"لا تُعيِّن ",[18,17112,17113],{},"Content-Type"," عند إرسال ",[18,17116,17117],{},"FormData"," المتصفح يحتاج إلى أن يختار سلسلة boundary الخاصة به لترميز multipart. لو ضبطتَ ",[18,17120,17121],{},"Content-Type: multipart\u002Fform-data"," يدوياً، فستُلغي القيمة التي يضعها المتصفح ومعها الـ boundary، وسيفشل الخادم في تحليل الجسم.",[3673,17124,17125,8698,17131,17133],{},[36,17126,17127,17128,17130],{},"فحص ",[18,17129,16460],{}," يجب أن يكون صريحاً.",[18,17132,16350],{}," يُطلَق حتى مع 500. الخلط بين \"اكتمل الطلب\" و\"نجح الطلب\" هو المصدر الأوَّل والأكثر شيوعاً لعلل الرفع الصامتة.",[3673,17135,17136,17142,17143,17146],{},[36,17137,17138,17141],{},[18,17139,17140],{},"new Promise"," هو المحوِّل."," XHR مبني على الأحداث، وبقية شيفرتك بشكل ",[18,17144,17145],{},"async\u002Fawait",". تغليفه مرَّة واحدة داخل Promise يُبقي هذا الحرج محصوراً في مكانه.",[26,17148,17150],{"id":17149},"أبسط-مثال-لتقدُّم-التنزيل","أبسط مثال لتقدُّم التنزيل",[11,17152,17153,17154,337],{},"الشكل نفسه بالنسبة إلى التنزيل، لكن على ",[18,17155,17156],{},"xhr.onprogress",[40,17158,17160],{"className":1893,"code":17159,"language":1895,"meta":45,"style":45},"async function downloadWithProgress(\n  url: string,\n  onProgress: (loaded: number, total: number | null) => void\n): Promise\u003CBlob> {\n  return new Promise\u003CBlob>((resolve, reject) => {\n    const xhr = new XMLHttpRequest();\n    xhr.open('GET', url, true);\n    xhr.responseType = 'blob';\n\n    xhr.onprogress = (e) => {\n      const total = e.lengthComputable ? e.total : null;\n      onProgress(e.loaded, total);\n    };\n\n    xhr.onload = () => {\n      if (xhr.status >= 200 && xhr.status \u003C 300) {\n        resolve(xhr.response as Blob);\n      } else {\n        reject(new Error(`HTTP ${xhr.status}`));\n      }\n    };\n    xhr.onerror = () => reject(new TypeError('Network error'));\n\n    xhr.send();\n  });\n}\n",[18,17161,17162,17173,17184,17216,17231,17257,17271,17288,17300,17304,17322,17342,17348,17352,17356,17370,17390,17405,17414,17439,17443,17447,17473,17477,17485,17489],{"__ignoreMap":45},[49,17163,17164,17166,17168,17171],{"class":51,"line":52},[49,17165,3752],{"class":63},[49,17167,3347],{"class":63},[49,17169,17170],{"class":262}," downloadWithProgress",[49,17172,2114],{"class":55},[49,17174,17175,17178,17180,17182],{"class":51,"line":82},[49,17176,17177],{"class":601},"  url",[49,17179,337],{"class":63},[49,17181,607],{"class":333},[49,17183,360],{"class":55},[49,17185,17186,17188,17190,17192,17194,17196,17198,17200,17202,17204,17206,17208,17210,17212,17214],{"class":51,"line":88},[49,17187,16236],{"class":262},[49,17189,337],{"class":63},[49,17191,2183],{"class":55},[49,17193,16746],{"class":601},[49,17195,337],{"class":63},[49,17197,3775],{"class":333},[49,17199,159],{"class":55},[49,17201,16573],{"class":601},[49,17203,337],{"class":63},[49,17205,3775],{"class":333},[49,17207,679],{"class":63},[49,17209,657],{"class":333},[49,17211,807],{"class":55},[49,17213,648],{"class":63},[49,17215,16769],{"class":333},[49,17217,17218,17220,17222,17224,17226,17229],{"class":51,"line":94},[49,17219,2433],{"class":55},[49,17221,337],{"class":63},[49,17223,3370],{"class":262},[49,17225,56],{"class":55},[49,17227,17228],{"class":262},"Blob",[49,17230,3377],{"class":55},[49,17232,17233,17235,17237,17239,17241,17243,17245,17247,17249,17251,17253,17255],{"class":51,"line":363},[49,17234,2962],{"class":63},[49,17236,3838],{"class":63},[49,17238,3370],{"class":333},[49,17240,56],{"class":55},[49,17242,17228],{"class":262},[49,17244,16798],{"class":55},[49,17246,4238],{"class":601},[49,17248,159],{"class":55},[49,17250,16805],{"class":601},[49,17252,807],{"class":55},[49,17254,648],{"class":63},[49,17256,1625],{"class":55},[49,17258,17259,17261,17263,17265,17267,17269],{"class":51,"line":374},[49,17260,2204],{"class":63},[49,17262,16818],{"class":333},[49,17264,343],{"class":63},[49,17266,3838],{"class":63},[49,17268,16825],{"class":262},[49,17270,3477],{"class":55},[49,17272,17273,17275,17277,17279,17281,17284,17286],{"class":51,"line":385},[49,17274,16832],{"class":55},[49,17276,4291],{"class":262},[49,17278,625],{"class":55},[49,17280,12928],{"class":70},[49,17282,17283],{"class":55},", url, ",[49,17285,774],{"class":333},[49,17287,630],{"class":55},[49,17289,17290,17293,17295,17298],{"class":51,"line":662},[49,17291,17292],{"class":55},"    xhr.responseType ",[49,17294,67],{"class":63},[49,17296,17297],{"class":70}," 'blob'",[49,17299,319],{"class":55},[49,17301,17302],{"class":51,"line":691},[49,17303,325],{"emptyLinePlaceholder":324},[49,17305,17306,17308,17310,17312,17314,17316,17318,17320],{"class":51,"line":696},[49,17307,16832],{"class":55},[49,17309,16292],{"class":262},[49,17311,343],{"class":63},[49,17313,2183],{"class":55},[49,17315,13309],{"class":601},[49,17317,807],{"class":55},[49,17319,648],{"class":63},[49,17321,1625],{"class":55},[49,17323,17324,17326,17328,17330,17332,17334,17336,17338,17340],{"class":51,"line":723},[49,17325,2521],{"class":63},[49,17327,16113],{"class":333},[49,17329,343],{"class":63},[49,17331,16635],{"class":55},[49,17333,758],{"class":63},[49,17335,16640],{"class":55},[49,17337,337],{"class":63},[49,17339,657],{"class":333},[49,17341,319],{"class":55},[49,17343,17344,17346],{"class":51,"line":744},[49,17345,16898],{"class":262},[49,17347,16901],{"class":55},[49,17349,17350],{"class":51,"line":768},[49,17351,6766],{"class":55},[49,17353,17354],{"class":51,"line":779},[49,17355,325],{"emptyLinePlaceholder":324},[49,17357,17358,17360,17362,17364,17366,17368],{"class":51,"line":784},[49,17359,16832],{"class":55},[49,17361,16350],{"class":262},[49,17363,343],{"class":63},[49,17365,4441],{"class":55},[49,17367,648],{"class":63},[49,17369,1625],{"class":55},[49,17371,17372,17374,17376,17378,17380,17382,17384,17386,17388],{"class":51,"line":789},[49,17373,2154],{"class":63},[49,17375,16930],{"class":55},[49,17377,2808],{"class":63},[49,17379,15768],{"class":333},[49,17381,8933],{"class":63},[49,17383,16939],{"class":55},[49,17385,56],{"class":63},[49,17387,16944],{"class":333},[49,17389,2149],{"class":55},[49,17391,17392,17395,17398,17400,17403],{"class":51,"line":820},[49,17393,17394],{"class":262},"        resolve",[49,17396,17397],{"class":55},"(xhr.response ",[49,17399,2657],{"class":63},[49,17401,17402],{"class":262}," Blob",[49,17404,630],{"class":55},[49,17406,17407,17410,17412],{"class":51,"line":829},[49,17408,17409],{"class":55},"      } ",[49,17411,9449],{"class":63},[49,17413,1625],{"class":55},[49,17415,17416,17419,17421,17423,17425,17427,17429,17431,17433,17435,17437],{"class":51,"line":834},[49,17417,17418],{"class":262},"        reject",[49,17420,625],{"class":55},[49,17422,15242],{"class":63},[49,17424,13272],{"class":262},[49,17426,625],{"class":55},[49,17428,16969],{"class":70},[49,17430,16446],{"class":55},[49,17432,527],{"class":70},[49,17434,16976],{"class":55},[49,17436,739],{"class":70},[49,17438,16981],{"class":55},[49,17440,17441],{"class":51,"line":844},[49,17442,2565],{"class":55},[49,17444,17445],{"class":51,"line":861},[49,17446,6766],{"class":55},[49,17448,17449,17451,17453,17455,17457,17459,17461,17463,17465,17467,17469,17471],{"class":51,"line":888},[49,17450,16832],{"class":55},[49,17452,16371],{"class":262},[49,17454,343],{"class":63},[49,17456,4441],{"class":55},[49,17458,648],{"class":63},[49,17460,16958],{"class":262},[49,17462,625],{"class":55},[49,17464,15242],{"class":63},[49,17466,17006],{"class":262},[49,17468,625],{"class":55},[49,17470,17011],{"class":70},[49,17472,16981],{"class":55},[49,17474,17475],{"class":51,"line":906},[49,17476,325],{"emptyLinePlaceholder":324},[49,17478,17479,17481,17483],{"class":51,"line":927},[49,17480,16832],{"class":55},[49,17482,17089],{"class":262},[49,17484,3477],{"class":55},[49,17486,17487],{"class":51,"line":937},[49,17488,8036],{"class":55},[49,17490,17491],{"class":51,"line":2725},[49,17492,1719],{"class":55},[11,17494,17495,17498,17499,17502,17503,3728,17506,17509,17510,8698,17513,527],{},[18,17496,17497],{},"xhr.responseType = 'blob'"," هو السطر الحاسم عند تنزيل الملفات. بدونه ستحصل على نص، وفي المحتوى الثنائي يعني ذلك أن المتصفح سيفكُّ ترميز البايتات بـ UTF-8 ويسلِّمك محتوى فاسداً. مع JSON تستخدم ",[18,17500,17501],{},"'json'","، ومع البايتات العامة ",[18,17504,17505],{},"'arraybuffer'",[18,17507,17508],{},"'blob'",". المهم أن تضبطه ",[7077,17511,17512],{},"قبل",[18,17514,17515],{},"send()",[11,17517,17518,17519,17521,17522,17524,17525,17527,17528,17530,17531,17533],{},"كما أن على الخادم أن يُرسل ترويسة ",[18,17520,16566],{}," إن كنت تريد أن تكون قيمة ",[18,17523,16573],{}," ذات معنى. ترميز النقل المُقطَّع لا يتضمَّنها، فتخرج ",[18,17526,16487],{}," بقيمة ",[18,17529,11087],{},". وشبكات CDN التي تُطبِّق gzip أثناء التسليم تُسقط أحياناً ",[18,17532,16566],{}," بدورها. حين يحدث هذا، سترضى بعدَّاد بايتات بلا نسبة مئوية، وهذا الحل الأمين على أي حال.",[26,17535,17537,17538],{"id":17536},"تغليف-xhr-ليبدو-مثل-fetch","تغليف XHR ليبدو مثل ",[18,17539,13051],{},[11,17541,17542,17543,17545,17546,17548],{},"معظم قاعدة الشيفرة الحديثة مبنية على شكل ",[18,17544,13051],{},". وإذا نفَّذتَ التقدُّم الحقيقي عبر XHR وتركتَ كل شيء آخر على ",[18,17547,13051],{},"، فسينتهي بك الأمر إلى خطَّي طلب متوازيين بأنواع أخطاء مختلفة، ودلالات إلغاء مختلفة، وتطبيع headers مختلف.",[11,17550,17551,17552,17554,17555,17560],{},"والحل أن تُبقي ",[18,17553,13051],{}," هو الخيار الافتراضي، وأن تُبدِّله بدالة مبنية على XHR ",[7077,17556,17557,17558],{},"تُعيد ",[18,17559,14112],{}," في الحالات التي يُطلَب فيها التقدُّم فقط. بهذا لا تتغيَّر الشيفرة المستدعية إطلاقاً.",[11,17562,17563],{},"هذا غلاف يفعل ذلك تماماً (وهو التطبيق الفعلي المستخدم في حزمة api-provider خاصتي):",[40,17565,17567],{"className":1893,"code":17566,"language":1895,"meta":45,"style":45},"export function createXhrFetch(\n  onProgress: (progress: RequestProgress) => void\n): (input: string, init: RequestInit) => Promise\u003CResponse> {\n  return function xhrFetch(input, init) {\n    const method = (init.method ?? 'GET').toUpperCase();\n    const body = (init.body ?? null) as XMLHttpRequestBodyInit | null;\n    const headers = init.headers as Record\u003Cstring, string> | undefined;\n    const signal = init.signal as AbortSignal | null | undefined;\n\n    return new Promise\u003CResponse>((resolve, reject) => {\n      if (typeof XMLHttpRequest === 'undefined') {\n        reject(new Error('XHR unavailable in this runtime'));\n        return;\n      }\n\n      const xhr = new XMLHttpRequest();\n      xhr.open(method, input, true);\n      xhr.responseType = 'blob';\n      if (init.credentials === 'include') xhr.withCredentials = true;\n\n      if (headers) {\n        for (const [name, value] of Object.entries(headers)) {\n          try { xhr.setRequestHeader(name, value); }\n          catch { \u002F* forbidden headers are silently skipped *\u002F }\n        }\n      }\n\n      xhr.upload.onprogress = (e) => onProgress(normaliseProgress('upload', e));\n      xhr.onprogress        = (e) => onProgress(normaliseProgress('download', e));\n\n      const onAbort = () => xhr.abort();\n      if (signal) {\n        if (signal.aborted) {\n          reject(new DOMException('Aborted', 'AbortError'));\n          return;\n        }\n        signal.addEventListener('abort', onAbort, { once: true });\n      }\n\n      xhr.onload = () => {\n        if (signal) signal.removeEventListener('abort', onAbort);\n        resolve(new Response(xhr.response as BodyInit | null, {\n          status: xhr.status,\n          statusText: xhr.statusText,\n          headers: parseHeaders(xhr.getAllResponseHeaders()),\n        }));\n      };\n      xhr.onerror   = () => reject(new TypeError('Network error'));\n      xhr.onabort   = () => reject(new DOMException('Aborted', 'AbortError'));\n      xhr.ontimeout = () => reject(new DOMException('Request timeout', 'TimeoutError'));\n\n      xhr.send(body);\n    });\n  };\n}\n",[18,17568,17569,17579,17600,17635,17654,17677,17706,17737,17762,17766,17792,17809,17826,17832,17836,17840,17854,17868,17879,17900,17904,17911,17941,17955,17967,17971,17975,17979,18012,18041,18045,18064,18071,18078,18099,18105,18109,18126,18130,18134,18148,18165,18188,18193,18198,18215,18221,18227,18255,18286,18319,18324,18334,18339,18344],{"__ignoreMap":45},[49,17570,17571,17573,17575,17577],{"class":51,"line":52},[49,17572,3341],{"class":63},[49,17574,3347],{"class":63},[49,17576,13092],{"class":262},[49,17578,2114],{"class":55},[49,17580,17581,17583,17585,17587,17589,17591,17594,17596,17598],{"class":51,"line":82},[49,17582,16236],{"class":262},[49,17584,337],{"class":63},[49,17586,2183],{"class":55},[49,17588,13062],{"class":601},[49,17590,337],{"class":63},[49,17592,17593],{"class":262}," RequestProgress",[49,17595,807],{"class":55},[49,17597,648],{"class":63},[49,17599,16769],{"class":333},[49,17601,17602,17604,17606,17608,17611,17613,17615,17617,17619,17621,17623,17625,17627,17629,17631,17633],{"class":51,"line":88},[49,17603,2433],{"class":55},[49,17605,337],{"class":63},[49,17607,2183],{"class":55},[49,17609,17610],{"class":601},"input",[49,17612,337],{"class":63},[49,17614,607],{"class":333},[49,17616,159],{"class":55},[49,17618,14382],{"class":601},[49,17620,337],{"class":63},[49,17622,14387],{"class":262},[49,17624,807],{"class":55},[49,17626,648],{"class":63},[49,17628,3370],{"class":262},[49,17630,56],{"class":55},[49,17632,14112],{"class":262},[49,17634,3377],{"class":55},[49,17636,17637,17639,17641,17644,17646,17648,17650,17652],{"class":51,"line":94},[49,17638,2962],{"class":63},[49,17640,3347],{"class":63},[49,17642,17643],{"class":262}," xhrFetch",[49,17645,625],{"class":55},[49,17647,17610],{"class":601},[49,17649,159],{"class":55},[49,17651,14382],{"class":601},[49,17653,2149],{"class":55},[49,17655,17656,17658,17661,17663,17666,17668,17671,17673,17675],{"class":51,"line":363},[49,17657,2204],{"class":63},[49,17659,17660],{"class":333}," method",[49,17662,343],{"class":63},[49,17664,17665],{"class":55}," (init.method ",[49,17667,2555],{"class":63},[49,17669,17670],{"class":70}," 'GET'",[49,17672,1251],{"class":55},[49,17674,3521],{"class":262},[49,17676,3477],{"class":55},[49,17678,17679,17681,17684,17686,17689,17691,17693,17695,17697,17700,17702,17704],{"class":51,"line":374},[49,17680,2204],{"class":63},[49,17682,17683],{"class":333}," body",[49,17685,343],{"class":63},[49,17687,17688],{"class":55}," (init.body ",[49,17690,2555],{"class":63},[49,17692,657],{"class":333},[49,17694,807],{"class":55},[49,17696,2657],{"class":63},[49,17698,17699],{"class":262}," XMLHttpRequestBodyInit",[49,17701,679],{"class":63},[49,17703,657],{"class":333},[49,17705,319],{"class":55},[49,17707,17708,17710,17712,17714,17717,17719,17721,17723,17725,17727,17729,17731,17733,17735],{"class":51,"line":385},[49,17709,2204],{"class":63},[49,17711,14630],{"class":333},[49,17713,343],{"class":63},[49,17715,17716],{"class":55}," init.headers ",[49,17718,2657],{"class":63},[49,17720,5455],{"class":262},[49,17722,56],{"class":55},[49,17724,1914],{"class":333},[49,17726,159],{"class":55},[49,17728,1914],{"class":333},[49,17730,5553],{"class":55},[49,17732,5433],{"class":63},[49,17734,5440],{"class":333},[49,17736,319],{"class":55},[49,17738,17739,17741,17743,17745,17748,17750,17752,17754,17756,17758,17760],{"class":51,"line":662},[49,17740,2204],{"class":63},[49,17742,14771],{"class":333},[49,17744,343],{"class":63},[49,17746,17747],{"class":55}," init.signal ",[49,17749,2657],{"class":63},[49,17751,14825],{"class":262},[49,17753,679],{"class":63},[49,17755,657],{"class":333},[49,17757,679],{"class":63},[49,17759,5440],{"class":333},[49,17761,319],{"class":55},[49,17763,17764],{"class":51,"line":691},[49,17765,325],{"emptyLinePlaceholder":324},[49,17767,17768,17770,17772,17774,17776,17778,17780,17782,17784,17786,17788,17790],{"class":51,"line":696},[49,17769,2945],{"class":63},[49,17771,3838],{"class":63},[49,17773,3370],{"class":333},[49,17775,56],{"class":55},[49,17777,14112],{"class":262},[49,17779,16798],{"class":55},[49,17781,4238],{"class":601},[49,17783,159],{"class":55},[49,17785,16805],{"class":601},[49,17787,807],{"class":55},[49,17789,648],{"class":63},[49,17791,1625],{"class":55},[49,17793,17794,17796,17798,17800,17803,17805,17807],{"class":51,"line":723},[49,17795,2154],{"class":63},[49,17797,2183],{"class":55},[49,17799,6713],{"class":63},[49,17801,17802],{"class":55}," XMLHttpRequest ",[49,17804,654],{"class":63},[49,17806,8471],{"class":70},[49,17808,2149],{"class":55},[49,17810,17811,17813,17815,17817,17819,17821,17824],{"class":51,"line":744},[49,17812,17418],{"class":262},[49,17814,625],{"class":55},[49,17816,15242],{"class":63},[49,17818,13272],{"class":262},[49,17820,625],{"class":55},[49,17822,17823],{"class":70},"'XHR unavailable in this runtime'",[49,17825,16981],{"class":55},[49,17827,17828,17830],{"class":51,"line":768},[49,17829,2544],{"class":63},[49,17831,319],{"class":55},[49,17833,17834],{"class":51,"line":779},[49,17835,2565],{"class":55},[49,17837,17838],{"class":51,"line":784},[49,17839,325],{"emptyLinePlaceholder":324},[49,17841,17842,17844,17846,17848,17850,17852],{"class":51,"line":789},[49,17843,2521],{"class":63},[49,17845,16818],{"class":333},[49,17847,343],{"class":63},[49,17849,3838],{"class":63},[49,17851,16825],{"class":262},[49,17853,3477],{"class":55},[49,17855,17856,17859,17861,17864,17866],{"class":51,"line":820},[49,17857,17858],{"class":55},"      xhr.",[49,17860,4291],{"class":262},[49,17862,17863],{"class":55},"(method, input, ",[49,17865,774],{"class":333},[49,17867,630],{"class":55},[49,17869,17870,17873,17875,17877],{"class":51,"line":829},[49,17871,17872],{"class":55},"      xhr.responseType ",[49,17874,67],{"class":63},[49,17876,17297],{"class":70},[49,17878,319],{"class":55},[49,17880,17881,17883,17886,17888,17891,17894,17896,17898],{"class":51,"line":834},[49,17882,2154],{"class":63},[49,17884,17885],{"class":55}," (init.credentials ",[49,17887,654],{"class":63},[49,17889,17890],{"class":70}," 'include'",[49,17892,17893],{"class":55},") xhr.withCredentials ",[49,17895,67],{"class":63},[49,17897,3174],{"class":333},[49,17899,319],{"class":55},[49,17901,17902],{"class":51,"line":844},[49,17903,325],{"emptyLinePlaceholder":324},[49,17905,17906,17908],{"class":51,"line":861},[49,17907,2154],{"class":63},[49,17909,17910],{"class":55}," (headers) {\n",[49,17912,17913,17916,17918,17920,17922,17925,17927,17929,17931,17934,17936,17938],{"class":51,"line":888},[49,17914,17915],{"class":63},"        for",[49,17917,2183],{"class":55},[49,17919,330],{"class":63},[49,17921,7654],{"class":55},[49,17923,17924],{"class":333},"name",[49,17926,159],{"class":55},[49,17928,4892],{"class":333},[49,17930,13287],{"class":55},[49,17932,17933],{"class":63},"of",[49,17935,6075],{"class":55},[49,17937,6078],{"class":262},[49,17939,17940],{"class":55},"(headers)) {\n",[49,17942,17943,17946,17949,17952],{"class":51,"line":906},[49,17944,17945],{"class":63},"          try",[49,17947,17948],{"class":55}," { xhr.",[49,17950,17951],{"class":262},"setRequestHeader",[49,17953,17954],{"class":55},"(name, value); }\n",[49,17956,17957,17960,17962,17965],{"class":51,"line":927},[49,17958,17959],{"class":63},"          catch",[49,17961,701],{"class":55},[49,17963,17964],{"class":2472},"\u002F* forbidden headers are silently skipped *\u002F",[49,17966,2247],{"class":55},[49,17968,17969],{"class":51,"line":937},[49,17970,2728],{"class":55},[49,17972,17973],{"class":51,"line":2725},[49,17974,2565],{"class":55},[49,17976,17977],{"class":51,"line":2731},[49,17978,325],{"emptyLinePlaceholder":324},[49,17980,17981,17984,17986,17988,17990,17992,17994,17996,17999,18001,18004,18006,18009],{"class":51,"line":2736},[49,17982,17983],{"class":55},"      xhr.upload.",[49,17985,16292],{"class":262},[49,17987,343],{"class":63},[49,17989,2183],{"class":55},[49,17991,13309],{"class":601},[49,17993,807],{"class":55},[49,17995,648],{"class":63},[49,17997,17998],{"class":262}," onProgress",[49,18000,625],{"class":55},[49,18002,18003],{"class":262},"normaliseProgress",[49,18005,625],{"class":55},[49,18007,18008],{"class":70},"'upload'",[49,18010,18011],{"class":55},", e));\n",[49,18013,18014,18016,18018,18020,18022,18024,18026,18028,18030,18032,18034,18036,18039],{"class":51,"line":2746},[49,18015,17858],{"class":55},[49,18017,16292],{"class":262},[49,18019,16324],{"class":63},[49,18021,2183],{"class":55},[49,18023,13309],{"class":601},[49,18025,807],{"class":55},[49,18027,648],{"class":63},[49,18029,17998],{"class":262},[49,18031,625],{"class":55},[49,18033,18003],{"class":262},[49,18035,625],{"class":55},[49,18037,18038],{"class":70},"'download'",[49,18040,18011],{"class":55},[49,18042,18043],{"class":51,"line":2752},[49,18044,325],{"emptyLinePlaceholder":324},[49,18046,18047,18049,18051,18053,18055,18057,18060,18062],{"class":51,"line":2757},[49,18048,2521],{"class":63},[49,18050,14927],{"class":262},[49,18052,343],{"class":63},[49,18054,4441],{"class":55},[49,18056,648],{"class":63},[49,18058,18059],{"class":55}," xhr.",[49,18061,3865],{"class":262},[49,18063,3477],{"class":55},[49,18065,18066,18068],{"class":51,"line":2762},[49,18067,2154],{"class":63},[49,18069,18070],{"class":55}," (signal) {\n",[49,18072,18073,18075],{"class":51,"line":2767},[49,18074,2701],{"class":63},[49,18076,18077],{"class":55}," (signal.aborted) {\n",[49,18079,18080,18083,18085,18087,18089,18091,18093,18095,18097],{"class":51,"line":2773},[49,18081,18082],{"class":262},"          reject",[49,18084,625],{"class":55},[49,18086,15242],{"class":63},[49,18088,17034],{"class":262},[49,18090,625],{"class":55},[49,18092,17039],{"class":70},[49,18094,159],{"class":55},[49,18096,17044],{"class":70},[49,18098,16981],{"class":55},[49,18100,18101,18103],{"class":51,"line":2822},[49,18102,2709],{"class":63},[49,18104,319],{"class":55},[49,18106,18107],{"class":51,"line":2845},[49,18108,2728],{"class":55},[49,18110,18111,18114,18116,18118,18120,18122,18124],{"class":51,"line":2861},[49,18112,18113],{"class":55},"        signal.",[49,18115,11887],{"class":262},[49,18117,625],{"class":55},[49,18119,14951],{"class":70},[49,18121,14954],{"class":55},[49,18123,774],{"class":333},[49,18125,1927],{"class":55},[49,18127,18128],{"class":51,"line":2882},[49,18129,2565],{"class":55},[49,18131,18132],{"class":51,"line":2903},[49,18133,325],{"emptyLinePlaceholder":324},[49,18135,18136,18138,18140,18142,18144,18146],{"class":51,"line":2930},[49,18137,17858],{"class":55},[49,18139,16350],{"class":262},[49,18141,343],{"class":63},[49,18143,4441],{"class":55},[49,18145,648],{"class":63},[49,18147,1625],{"class":55},[49,18149,18150,18152,18155,18158,18160,18162],{"class":51,"line":2936},[49,18151,2701],{"class":63},[49,18153,18154],{"class":55}," (signal) signal.",[49,18156,18157],{"class":262},"removeEventListener",[49,18159,625],{"class":55},[49,18161,14951],{"class":70},[49,18163,18164],{"class":55},", onAbort);\n",[49,18166,18167,18169,18171,18173,18175,18177,18179,18182,18184,18186],{"class":51,"line":2942},[49,18168,17394],{"class":262},[49,18170,625],{"class":55},[49,18172,15242],{"class":63},[49,18174,15081],{"class":262},[49,18176,17397],{"class":55},[49,18178,2657],{"class":63},[49,18180,18181],{"class":262}," BodyInit",[49,18183,679],{"class":63},[49,18185,657],{"class":333},[49,18187,7993],{"class":55},[49,18189,18190],{"class":51,"line":2954},[49,18191,18192],{"class":55},"          status: xhr.status,\n",[49,18194,18195],{"class":51,"line":2959},[49,18196,18197],{"class":55},"          statusText: xhr.statusText,\n",[49,18199,18200,18203,18206,18209,18212],{"class":51,"line":2969},[49,18201,18202],{"class":55},"          headers: ",[49,18204,18205],{"class":262},"parseHeaders",[49,18207,18208],{"class":55},"(xhr.",[49,18210,18211],{"class":262},"getAllResponseHeaders",[49,18213,18214],{"class":55},"()),\n",[49,18216,18218],{"class":51,"line":18217},46,[49,18219,18220],{"class":55},"        }));\n",[49,18222,18224],{"class":51,"line":18223},47,[49,18225,18226],{"class":55},"      };\n",[49,18228,18230,18232,18234,18237,18239,18241,18243,18245,18247,18249,18251,18253],{"class":51,"line":18229},48,[49,18231,17858],{"class":55},[49,18233,16371],{"class":262},[49,18235,18236],{"class":63},"   =",[49,18238,4441],{"class":55},[49,18240,648],{"class":63},[49,18242,16958],{"class":262},[49,18244,625],{"class":55},[49,18246,15242],{"class":63},[49,18248,17006],{"class":262},[49,18250,625],{"class":55},[49,18252,17011],{"class":70},[49,18254,16981],{"class":55},[49,18256,18258,18260,18262,18264,18266,18268,18270,18272,18274,18276,18278,18280,18282,18284],{"class":51,"line":18257},49,[49,18259,17858],{"class":55},[49,18261,16392],{"class":262},[49,18263,18236],{"class":63},[49,18265,4441],{"class":55},[49,18267,648],{"class":63},[49,18269,16958],{"class":262},[49,18271,625],{"class":55},[49,18273,15242],{"class":63},[49,18275,17034],{"class":262},[49,18277,625],{"class":55},[49,18279,17039],{"class":70},[49,18281,159],{"class":55},[49,18283,17044],{"class":70},[49,18285,16981],{"class":55},[49,18287,18289,18291,18293,18295,18297,18299,18301,18303,18305,18307,18309,18312,18314,18317],{"class":51,"line":18288},50,[49,18290,17858],{"class":55},[49,18292,16412],{"class":262},[49,18294,343],{"class":63},[49,18296,4441],{"class":55},[49,18298,648],{"class":63},[49,18300,16958],{"class":262},[49,18302,625],{"class":55},[49,18304,15242],{"class":63},[49,18306,17034],{"class":262},[49,18308,625],{"class":55},[49,18310,18311],{"class":70},"'Request timeout'",[49,18313,159],{"class":55},[49,18315,18316],{"class":70},"'TimeoutError'",[49,18318,16981],{"class":55},[49,18320,18322],{"class":51,"line":18321},51,[49,18323,325],{"emptyLinePlaceholder":324},[49,18325,18327,18329,18331],{"class":51,"line":18326},52,[49,18328,17858],{"class":55},[49,18330,17089],{"class":262},[49,18332,18333],{"class":55},"(body);\n",[49,18335,18337],{"class":51,"line":18336},53,[49,18338,12610],{"class":55},[49,18340,18342],{"class":51,"line":18341},54,[49,18343,12861],{"class":55},[49,18345,18347],{"class":51,"line":18346},55,[49,18348,1719],{"class":55},[11,18350,18351],{},"أربعة أمور تقوم بالعمل الحقيقي في هذا الشكل:",[11,18353,18354,18355,18360,18361,18364],{},"أوَّلها أن ",[36,18356,18357,18358],{},"نوع الإرجاع ",[18,18359,14193],{},". وهذا هو المقصد كله: كل ما يقبل ",[18,18362,18363],{},"typeof fetch"," سيقبل هذه الدالة، وتبقى بقية قاعدة الشيفرة (interceptors وretry وتحليل JSON وتحويل الأخطاء) لا تُبالي بالناقل المستخدم.",[11,18366,18367,18368,18371,18372,18375,18376,18379,18380,18382,18383,337],{},"وثانيها ",[36,18369,18370],{},"مطبِّع الترويسات في الأسفل","، إذ يُترجم ",[18,18373,18374],{},"xhr.getAllResponseHeaders()"," (سلسلة خام مفصولة بـ CRLF) إلى كائن ",[18,18377,18378],{},"Headers","، بحيث يسلك ",[18,18381,14112],{}," سلوكه المعتاد كأنه قادم من ",[18,18384,13051],{},[40,18386,18388],{"className":1893,"code":18387,"language":1895,"meta":45,"style":45},"function parseHeaders(raw: string): Headers {\n  const out = new Headers();\n  if (!raw) return out;\n  for (const line of raw.trim().split(\u002F[\\r\\n]+\u002F)) {\n    const idx = line.indexOf(':');\n    if (idx \u003C 0) continue;\n    const name = line.slice(0, idx).trim();\n    const value = line.slice(idx + 1).trim();\n    if (name) out.append(name, value);\n  }\n  return out;\n}\n",[18,18389,18390,18414,18429,18445,18481,18502,18519,18542,18567,18579,18583,18589],{"__ignoreMap":45},[49,18391,18392,18394,18397,18399,18401,18403,18405,18407,18409,18412],{"class":51,"line":52},[49,18393,2418],{"class":63},[49,18395,18396],{"class":262}," parseHeaders",[49,18398,625],{"class":55},[49,18400,5782],{"class":601},[49,18402,337],{"class":63},[49,18404,607],{"class":333},[49,18406,2433],{"class":55},[49,18408,337],{"class":63},[49,18410,18411],{"class":262}," Headers",[49,18413,1625],{"class":55},[49,18415,18416,18418,18421,18423,18425,18427],{"class":51,"line":82},[49,18417,2603],{"class":63},[49,18419,18420],{"class":333}," out",[49,18422,343],{"class":63},[49,18424,3838],{"class":63},[49,18426,18411],{"class":262},[49,18428,3477],{"class":55},[49,18430,18431,18433,18435,18437,18440,18442],{"class":51,"line":88},[49,18432,2449],{"class":63},[49,18434,2183],{"class":55},[49,18436,2454],{"class":63},[49,18438,18439],{"class":55},"raw) ",[49,18441,2197],{"class":63},[49,18443,18444],{"class":55}," out;\n",[49,18446,18447,18449,18451,18453,18456,18458,18461,18463,18465,18467,18469,18471,18474,18476,18478],{"class":51,"line":94},[49,18448,2776],{"class":63},[49,18450,2183],{"class":55},[49,18452,330],{"class":63},[49,18454,18455],{"class":333}," line",[49,18457,15309],{"class":63},[49,18459,18460],{"class":55}," raw.",[49,18462,5826],{"class":262},[49,18464,3989],{"class":55},[49,18466,8194],{"class":262},[49,18468,625],{"class":55},[49,18470,4011],{"class":70},[49,18472,18473],{"class":333},"[\\r\\n]",[49,18475,2393],{"class":63},[49,18477,4011],{"class":70},[49,18479,18480],{"class":55},")) {\n",[49,18482,18483,18485,18488,18490,18493,18495,18497,18500],{"class":51,"line":363},[49,18484,2204],{"class":63},[49,18486,18487],{"class":333}," idx",[49,18489,343],{"class":63},[49,18491,18492],{"class":55}," line.",[49,18494,15470],{"class":262},[49,18496,625],{"class":55},[49,18498,18499],{"class":70},"':'",[49,18501,630],{"class":55},[49,18503,18504,18506,18509,18511,18513,18515,18517],{"class":51,"line":374},[49,18505,2138],{"class":63},[49,18507,18508],{"class":55}," (idx ",[49,18510,56],{"class":63},[49,18512,8943],{"class":333},[49,18514,807],{"class":55},[49,18516,2877],{"class":63},[49,18518,319],{"class":55},[49,18520,18521,18523,18525,18527,18529,18531,18533,18535,18538,18540],{"class":51,"line":385},[49,18522,2204],{"class":63},[49,18524,5082],{"class":333},[49,18526,343],{"class":63},[49,18528,18492],{"class":55},[49,18530,2834],{"class":262},[49,18532,625],{"class":55},[49,18534,2839],{"class":333},[49,18536,18537],{"class":55},", idx).",[49,18539,5826],{"class":262},[49,18541,3477],{"class":55},[49,18543,18544,18546,18548,18550,18552,18554,18557,18559,18561,18563,18565],{"class":51,"line":662},[49,18545,2204],{"class":63},[49,18547,12100],{"class":333},[49,18549,343],{"class":63},[49,18551,18492],{"class":55},[49,18553,2834],{"class":262},[49,18555,18556],{"class":55},"(idx ",[49,18558,2393],{"class":63},[49,18560,2811],{"class":333},[49,18562,1251],{"class":55},[49,18564,5826],{"class":262},[49,18566,3477],{"class":55},[49,18568,18569,18571,18574,18576],{"class":51,"line":691},[49,18570,2138],{"class":63},[49,18572,18573],{"class":55}," (name) out.",[49,18575,17074],{"class":262},[49,18577,18578],{"class":55},"(name, value);\n",[49,18580,18581],{"class":51,"line":696},[49,18582,2589],{"class":55},[49,18584,18585,18587],{"class":51,"line":723},[49,18586,2962],{"class":63},[49,18588,18444],{"class":55},[49,18590,18591],{"class":51,"line":744},[49,18592,1719],{"class":55},[11,18594,18595,18596,18601,18602,18604,18605,18608,18609,18612,18613,18616],{},"وثالثها ",[36,18597,18598,18599],{},"جسر ",[18,18600,14754],{},"، فهو الذي يترجم بين بدائيّ الإلغاء في ",[18,18603,13051],{}," وتابع ",[18,18606,18607],{},"abort()"," في XHR. استدعاء ",[18,18610,18611],{},"AbortSignal.addEventListener('abort', xhr.abort)"," يجعل الواجهتين تتحدَّثان اللغة نفسها، وفحص ",[18,18614,18615],{},"signal.aborted"," منذ البداية يُعالج حالة أن يمرِّر المستدعي إشارة مُلغاة سلفاً (وتحدث مثلاً حين تُلغي طلباً قبل أن يُرسَل).",[11,18618,18619,18620,18627,18628,394,18631,394,18634,18637,18638,394,18641,8688,18644,18646,18647,18649],{},"ورابعها ",[36,18621,18622,18624,18625],{},[18,18623,14997],{}," حول ",[18,18626,17951],{},"، وهو مقصود. المتصفحات تمنع ترويسات معيَّنة مثل ",[18,18629,18630],{},"User-Agent",[18,18632,18633],{},"Cookie",[18,18635,18636],{},"Host"," ومعظم ",[18,18639,18640],{},"Sec-*",[18,18642,18643],{},"Proxy-*",[18,18645,13051],{}," يتجاهلها بصمت هو الآخر، و",[18,18648,14997],{}," هنا يجعل سلوك XHR مطابقاً له.",[26,18651,18653],{"id":18652},"الإلغاء-وانقضاء-المهلة-وحدوث-الخطأ","الإلغاء، وانقضاء المهلة، وحدوث الخطأ",[11,18655,18656,18657,394,18659,394,18661,394,18663,18665,18666,18668],{},"نهايات XHR الأربع الحصرية هي ",[18,18658,16350],{},[18,18660,16371],{},[18,18662,16392],{},[18,18664,16412],{},". وهي متنافية تماماً، بمعنى أن حدثاً واحداً بالضبط سيُطلَق لكل طلب. أما الترجمة إلى عالم ",[18,18667,13051],{}," فهي:",[1516,18670,18671,18684],{},[1519,18672,18673],{},[1522,18674,18675,18678,18681],{},[1525,18676,18677],{},"حدث XHR",[1525,18679,18680],{},"المعنى",[1525,18682,18683],{},"شكل الـ Promise",[1532,18685,18686,18700,18714,18731],{},[1522,18687,18688,18692,18695],{},[1537,18689,18690],{},[18,18691,16350],{},[1537,18693,18694],{},"ردَّ الخادم (بأي حالة)",[1537,18696,18697],{},[18,18698,18699],{},"resolve(new Response(...))",[1522,18701,18702,18706,18709],{},[1537,18703,18704],{},[18,18705,16371],{},[1537,18707,18708],{},"فشل شبكي، أو حجب CORS، أو DNS",[1537,18710,18711],{},[18,18712,18713],{},"reject(new TypeError('Network error'))",[1522,18715,18716,18720,18726],{},[1537,18717,18718],{},[18,18719,16392],{},[1537,18721,18722,18723],{},"استُدعي ",[18,18724,18725],{},"xhr.abort()",[1537,18727,18728],{},[18,18729,18730],{},"reject(new DOMException('...', 'AbortError'))",[1522,18732,18733,18737,18744],{},[1537,18734,18735],{},[18,18736,16412],{},[1537,18738,18739,18740,18743],{},"انقضت المدة المحددة في ",[18,18741,18742],{},"xhr.timeout"," بالمللي ثانية",[1537,18745,18746],{},[18,18747,18748],{},"reject(new DOMException('...', 'TimeoutError'))",[11,18750,18751,18753,18754,18757,18758,18761],{},[18,18752,13051],{}," يستخدم ",[18,18755,18756],{},"TypeError"," للأخطاء الشبكية و",[18,18759,18760],{},"DOMException"," للإلغاء. الحفاظ على هذه الأنواع بعينها يعني أن معالجات الأخطاء عندك ستستمر في العمل حين تُبدِّل الناقل.",[11,18763,18764,18765,18768,18769,18771,18772,523,18774,394,18776,18778],{},"وحين يضبط المستدعي ",[18,18766,18767],{},"xhr.timeout = 30_000","، سيُطلَق ",[18,18770,16412],{}," تلقائياً دون أي شيفرة إضافية. وهذه من المكاسب الصغيرة لـ XHR على ",[18,18773,13051],{},[18,18775,4077],{},[18,18777,14239],{},": آلية المهلة مدمجة أصلاً.",[26,18780,18782],{"id":18781},"مطبَّات-تستحق-أن-تُسمَّى","مطبَّات تستحق أن تُسمَّى",[11,18784,18785],{},"مجموعة صغيرة من الفخاخ كلَّفتني وقتاً في أول مرَّة وقعتُ فيها.",[11,18787,18788,18794,18795,18798],{},[36,18789,18790,18791,18793],{},"تعيين ",[18,18792,17113],{}," مع FormData يكسر multipart."," ترويسة ",[18,18796,18797],{},"multipart\u002Fform-data; boundary=..."," التي يُنشئها المتصفح تلقائياً هي الوحيدة التي يستطيع الخادم أن يحلِّلها. لا تتجاوزها.",[11,18800,18801,18804,18805,18808,18809,18812],{},[36,18802,18803],{},"تقدُّم CORS مقيَّد."," في الطلبات عبر الأصول، لن تُطلَق أحداث تقدُّم الرفع إلا إذا شملت الاستجابة ترويسة ",[18,18806,18807],{},"Access-Control-Allow-Origin"," المناسبة. فإن كنت ترى ",[18,18810,18811],{},"loaded = 0"," بلا نهاية في رفع عبر أصل مختلف، فالسبب غالباً CORS لا شيفرتك.",[11,18814,18815,18823],{},[36,18816,18817,18820,18821,527],{},[18,18818,18819],{},"responseType"," يجب أن يُضبط قبل ",[18,18822,17515],{}," ضبطه بعد ذلك لا يفعل شيئاً. المتصفح يحتاج إلى معرفة كيفية تخزين الاستجابة منذ أول بايت.",[11,18825,18826,18831,18832,18834,18835,18838],{},[36,18827,18828,18829,527],{},"gzip وترويسة ",[18,18830,16566],{}," كثير من شبكات CDN تُسقط ",[18,18833,16566],{}," حين تُطبِّق gzip أثناء التسليم، لأن الطول المضغوط يختلف عن غير المضغوط. فيرى عميلك ",[18,18836,18837],{},"lengthComputable: false"," رغم أن الخادم يعرف الحجم الحقيقي. لا يوجد حلٌّ من جانب العميل هنا. فإن كان الخادم بيدك، فقدِّم محتوى مضغوطاً مسبقاً، أو تجاوز الضغط في الأصول التي يهمُّ فيها التقدُّم.",[11,18840,18841,8698,18844,18847,18848,18851,18852,18855],{},[36,18842,18843],{},"لا تُبقي الاستجابة كلها في الذاكرة دون داعٍ.",[18,18845,18846],{},"responseType: 'blob'"," يترك للمتصفح إدارة العازلة، بينما ",[18,18849,18850],{},"responseType: 'arraybuffer'"," يُجبرها إلى heap الخاصة بـ JS. لأي شيء يتجاوز بضعة MB، ",[18,18853,18854],{},"blob"," هو الافتراضي الصحيح.",[26,18857,18859],{"id":18858},"ما-سألجأ-إليه-أولاً","ما سألجأ إليه أولاً",[11,18861,18862],{},"لأي مشروع يحتاج واجهات تقدُّم حقيقية، هذا هو الشكل الذي سأبنيه في كل مرَّة:",[8674,18864,18865,18873,18882,18890,18903],{},[3673,18866,18867,18872],{},[36,18868,18869,18870,527],{},"الناقل الافتراضي هو ",[18,18871,13051],{}," فهو البدائي الحديث، وهو الأسرع، ويتحوَّل إلى أنماط ودودة لـ SSR بنظافة.",[3673,18874,18875,18878,18879,18881],{},[36,18876,18877],{},"لا تلجأ إلى XHR إلا حين يطلب المستدعي التقدُّم صراحة."," خيار واحد باسم ",[18,18880,13066],{}," كافٍ لتبديل الناقل لهذا الاستدعاء وحده.",[3673,18883,18884,18889],{},[36,18885,18886,18887],{},"مسار XHR يُعيد ",[18,18888,14193],{},"، حتى يستمر كل ما بعده في العمل بلا تغيير: interceptors وretry وتحويل الأخطاء وتحليل JSON.",[3673,18891,18892,18899,18900,18902],{},[36,18893,18894,18895,18898],{},"طبِّع ",[18,18896,18897],{},"{ loaded, total, ratio }"," في طبقة النقل."," ومرِّر ",[18,18901,172],{}," إلى الأعلى حين يكون المجموع مجهولاً. ودع الواجهة تقرِّر كيف تعرض هذه الحالة.",[3673,18904,18905,18911,18912,18914,18915,18917],{},[36,18906,18907,18908,18910],{},"اجسر ",[18,18909,14754],{}," مرَّة واحدة"," في الغلاف، لا في كل موقع استدعاء. ",[18,18913,18607],{}," في XHR و",[18,18916,4077],{}," في fetch يشغلان الفتحة المنطقية نفسها، والغلاف هو الذي يُترجم بينهما.",[11,18919,18920],{},"هذه هي الصورة كاملة. لم يعد XHR الواجهة اللامعة، لكنه لا يزال يعمل في حالة محدَّدة يعجز عنها fetch، والقدر الصغير من شيفرة الغراء أعلاه هو ما يمنعه من أن يُلوِّث بقية قاعدة الشيفرة.",[219,18922,18923],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":45,"searchDepth":82,"depth":82,"links":18925},[18926,18928,18936,18938,18939,18941,18942,18943,18945,18946,18947],{"id":14173,"depth":82,"text":18927},"لماذا fetch الأصلي هو البدائي الافتراضي",{"id":14322,"depth":82,"text":14323,"children":18929},[18930,18931,18933,18934,18935],{"id":14344,"depth":88,"text":14345},{"id":14750,"depth":88,"text":18932},"الطبقة الثانية: المهلات عبر AbortSignal",{"id":15005,"depth":88,"text":15006},{"id":15712,"depth":88,"text":15713},{"id":15928,"depth":88,"text":15929},{"id":16072,"depth":82,"text":18937},"لماذا يعجز fetch عن إظهار تقدُّم الرفع",{"id":16275,"depth":82,"text":16276},{"id":16479,"depth":82,"text":18940},"شكل ProgressEvent، وحكاية lengthComputable",{"id":16703,"depth":82,"text":16704},{"id":17149,"depth":82,"text":17150},{"id":17536,"depth":82,"text":18944},"تغليف XHR ليبدو مثل fetch",{"id":18652,"depth":82,"text":18653},{"id":18781,"depth":82,"text":18782},{"id":18858,"depth":82,"text":18859},"2025-09-22","لماذا يبقى fetch الأصلي الخيار الافتراضي الصحيح، وكيف تبني حوله غلافاً متعدّد الطبقات (رابط أساسي، ومهلات، واعتراضات، وإعادة محاولات)، ومتى تظل بحاجة إلى XHR.",[18951,14137,18952,18953,18954,4077,14754,18955,14081,18956,18957,1846],"fetch API","XHR","تقدُّم الرفع","تقدُّم التنزيل","عميل HTTP مخصَّص","إعادة المحاولة","Exponential Backoff",{},"\u002Fwrites-ar\u002Fupload-and-download-progress-in-the-browser",{"title":14095,"description":18949},"writes-ar\u002Fupload-and-download-progress-in-the-browser","Rd7oX4pZes3Jt-NRZKY-28fuNjP69dDsvFWM37kHHgk",{"id":18964,"title":18965,"body":18966,"date":19330,"description":19331,"description_ar":1836,"draft":1837,"extension":1838,"image":1836,"keywords":19332,"lang":1849,"meta":19339,"navigation":324,"path":19340,"seo":19341,"stem":19342,"title_ar":1836,"updatedAt":1836,"__hash__":19343},"writesAr\u002Fwrites-ar\u002Fvalidating-image-urls-without-complex-error-handling.md","التحقق من روابط الصور دون معالجة أخطاء معقّدة",{"type":8,"value":18967,"toc":19325},[18968,18974,18981,18984,18987,19076,19080,19096,19161,19164,19168,19171,19289,19311,19314,19322],[11,18969,18970,18971,18973],{},"كل من عرض يوماً قائمة صور قادمة من بيانات لا يثق بها يعرف القصة جيداً. معظم الروابط تعمل، وواحد أو اثنان معطّلان، وأيقونة الصورة المكسورة تكفي وحدها لتخريب التصميم كاملاً. أمامك خياران: تلفّ كل شيء داخل ",[18,18972,14997],{},"، أو تسأل المتصفح مباشرةً وتدعه يجيب بنفسه: هل يمكنك تحميل هذا الرابط؟",[11,18975,18976,18977,18980],{},"هذا تحديداً ما كتبتُ ",[18,18978,18979],{},"checkUrl"," من أجله. تُعيد لك Promise ينجح تحليلها عند تحميل الصورة، ويُرفض عند فشلها. بلا fetch، وبلا صداع CORS، وبلا حاجة لإطلاق طلبات HEAD يدوياً.",[26,18982,18983],{"id":18983},"الفكرة",[11,18985,18986],{},"Promise ينجح تحليلها أو تُرفض بحسب ما إذا كانت الصورة قد تحمّلت:",[40,18988,18990],{"className":6465,"code":18989,"language":6467,"meta":45,"style":45},"\u002F\u002F success\nconst promise = new Promise\u003Cvoid>((resolve, reject) => { resolve() })\n\n\u002F\u002F failure\nconst promise = new Promise\u003Cvoid>((resolve, reject) => { reject() })\n",[18,18991,18992,18997,19033,19037,19042],{"__ignoreMap":45},[49,18993,18994],{"class":51,"line":52},[49,18995,18996],{"class":2472},"\u002F\u002F success\n",[49,18998,18999,19001,19004,19006,19008,19010,19012,19014,19016,19018,19020,19022,19024,19026,19028,19030],{"class":51,"line":82},[49,19000,330],{"class":63},[49,19002,19003],{"class":333}," promise",[49,19005,343],{"class":63},[49,19007,3838],{"class":63},[49,19009,3370],{"class":333},[49,19011,56],{"class":55},[49,19013,12326],{"class":333},[49,19015,16798],{"class":55},[49,19017,4238],{"class":601},[49,19019,159],{"class":55},[49,19021,16805],{"class":601},[49,19023,807],{"class":55},[49,19025,648],{"class":63},[49,19027,701],{"class":55},[49,19029,4238],{"class":262},[49,19031,19032],{"class":55},"() })\n",[49,19034,19035],{"class":51,"line":88},[49,19036,325],{"emptyLinePlaceholder":324},[49,19038,19039],{"class":51,"line":94},[49,19040,19041],{"class":2472},"\u002F\u002F failure\n",[49,19043,19044,19046,19048,19050,19052,19054,19056,19058,19060,19062,19064,19066,19068,19070,19072,19074],{"class":51,"line":363},[49,19045,330],{"class":63},[49,19047,19003],{"class":333},[49,19049,343],{"class":63},[49,19051,3838],{"class":63},[49,19053,3370],{"class":333},[49,19055,56],{"class":55},[49,19057,12326],{"class":333},[49,19059,16798],{"class":55},[49,19061,4238],{"class":601},[49,19063,159],{"class":55},[49,19065,16805],{"class":601},[49,19067,807],{"class":55},[49,19069,648],{"class":63},[49,19071,701],{"class":55},[49,19073,16805],{"class":262},[49,19075,19032],{"class":55},[26,19077,19079],{"id":19078},"الاستعانة-بكائن-image-الجاهز-في-المتصفح","الاستعانة بكائن Image الجاهز في المتصفح",[11,19081,19082,19083,19086,19087,394,19089,19091,19092,19095],{},"داخل الـ callback، أنشئ ",[18,19084,19085],{},"Image","، اربط ",[18,19088,16350],{},[18,19090,16371],{},"، ثم اضبط ",[18,19093,19094],{},"src"," ليبدأ الطلب.",[40,19097,19099],{"className":6465,"code":19098,"language":6467,"meta":45,"style":45},"const img = new Image();\nimg.onload = () => resolve();\nimg.onerror = () => reject();\nimg.src = url;\n",[18,19100,19101,19117,19135,19151],{"__ignoreMap":45},[49,19102,19103,19105,19108,19110,19112,19115],{"class":51,"line":52},[49,19104,330],{"class":63},[49,19106,19107],{"class":333}," img",[49,19109,343],{"class":63},[49,19111,3838],{"class":63},[49,19113,19114],{"class":262}," Image",[49,19116,3477],{"class":55},[49,19118,19119,19122,19124,19126,19128,19130,19133],{"class":51,"line":82},[49,19120,19121],{"class":55},"img.",[49,19123,16350],{"class":262},[49,19125,343],{"class":63},[49,19127,4441],{"class":55},[49,19129,648],{"class":63},[49,19131,19132],{"class":262}," resolve",[49,19134,3477],{"class":55},[49,19136,19137,19139,19141,19143,19145,19147,19149],{"class":51,"line":88},[49,19138,19121],{"class":55},[49,19140,16371],{"class":262},[49,19142,343],{"class":63},[49,19144,4441],{"class":55},[49,19146,648],{"class":63},[49,19148,16958],{"class":262},[49,19150,3477],{"class":55},[49,19152,19153,19156,19158],{"class":51,"line":94},[49,19154,19155],{"class":55},"img.src ",[49,19157,67],{"class":63},[49,19159,19160],{"class":55}," url;\n",[11,19162,19163],{},"هذه هي الحكاية كلها. المتصفح يتكفّل بالعمل الفعلي، وPromise مجرد غلاف رفيع حول حدثين لا أكثر.",[26,19165,19167],{"id":19166},"استخدامها-مع-صورة-احتياطية","استخدامها مع صورة احتياطية",[11,19169,19170],{},"الحالة الأكثر شيوعاً عندي هي التبديل إلى صورة احتياطية حين يكون الرابط الأصلي معطّلاً.",[40,19172,19174],{"className":6465,"code":19173,"language":6467,"meta":45,"style":45},"const fallbackImage = 'https:\u002F\u002Fexample.com\u002Ffallback.png';\nlet url: string = anonymousOBJECT.image;\n\nconst checkImage = async () => {\n  try {\n    await checkUrl(url);\n    \u002F\u002F valid, keep the original\n  } catch {\n    \u002F\u002F invalid, swap in the fallback\n    url = fallbackImage;\n  }\n};\n\ncheckImage();\n",[18,19175,19176,19190,19205,19209,19226,19232,19242,19247,19255,19260,19270,19274,19278,19282],{"__ignoreMap":45},[49,19177,19178,19180,19183,19185,19188],{"class":51,"line":52},[49,19179,330],{"class":63},[49,19181,19182],{"class":333}," fallbackImage",[49,19184,343],{"class":63},[49,19186,19187],{"class":70}," 'https:\u002F\u002Fexample.com\u002Ffallback.png'",[49,19189,319],{"class":55},[49,19191,19192,19194,19196,19198,19200,19202],{"class":51,"line":82},[49,19193,2781],{"class":63},[49,19195,14609],{"class":55},[49,19197,337],{"class":63},[49,19199,607],{"class":333},[49,19201,343],{"class":63},[49,19203,19204],{"class":55}," anonymousOBJECT.image;\n",[49,19206,19207],{"class":51,"line":88},[49,19208,325],{"emptyLinePlaceholder":324},[49,19210,19211,19213,19216,19218,19220,19222,19224],{"class":51,"line":94},[49,19212,330],{"class":63},[49,19214,19215],{"class":262}," checkImage",[49,19217,343],{"class":63},[49,19219,3344],{"class":63},[49,19221,4441],{"class":55},[49,19223,648],{"class":63},[49,19225,1625],{"class":55},[49,19227,19228,19230],{"class":51,"line":363},[49,19229,2478],{"class":63},[49,19231,1625],{"class":55},[49,19233,19234,19236,19239],{"class":51,"line":374},[49,19235,11735],{"class":63},[49,19237,19238],{"class":262}," checkUrl",[49,19240,19241],{"class":55},"(url);\n",[49,19243,19244],{"class":51,"line":385},[49,19245,19246],{"class":2472},"    \u002F\u002F valid, keep the original\n",[49,19248,19249,19251,19253],{"class":51,"line":662},[49,19250,2574],{"class":55},[49,19252,2577],{"class":63},[49,19254,1625],{"class":55},[49,19256,19257],{"class":51,"line":691},[49,19258,19259],{"class":2472},"    \u002F\u002F invalid, swap in the fallback\n",[49,19261,19262,19265,19267],{"class":51,"line":696},[49,19263,19264],{"class":55},"    url ",[49,19266,67],{"class":63},[49,19268,19269],{"class":55}," fallbackImage;\n",[49,19271,19272],{"class":51,"line":723},[49,19273,2589],{"class":55},[49,19275,19276],{"class":51,"line":744},[49,19277,5759],{"class":55},[49,19279,19280],{"class":51,"line":768},[49,19281,325],{"emptyLinePlaceholder":324},[49,19283,19284,19287],{"class":51,"line":779},[49,19285,19286],{"class":262},"checkImage",[49,19288,3477],{"class":55},[40,19290,19292],{"className":986,"code":19291,"language":988,"meta":45,"style":45},"\u003Cimg :src=\"url\" \u002F>\n",[18,19293,19294],{"__ignoreMap":45},[49,19295,19296,19298,19301,19304,19306,19309],{"class":51,"line":52},[49,19297,56],{"class":55},[49,19299,19300],{"class":59},"img",[49,19302,19303],{"class":262}," :src",[49,19305,67],{"class":55},[49,19307,19308],{"class":70},"\"url\"",[49,19310,885],{"class":55},[11,19312,19313],{},"لا معالجة أخطاء داخل القالب، ولا وميض مزعج لصورة مكسورة، والرابط يصل جاهزاً إلى الـ DOM.",[11,19315,19316,19317,527],{},"نُشر أصلاً على ",[6580,19318,19321],{"href":19319,"rel":19320},"https:\u002F\u002Fwww.linkedin.com\u002Fpulse\u002Fwithout-having-write-complex-error-handling-code-image-ali-abdelbaqy\u002F",[14045],"LinkedIn",[219,19323,19324],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":45,"searchDepth":82,"depth":82,"links":19326},[19327,19328,19329],{"id":18983,"depth":82,"text":18983},{"id":19078,"depth":82,"text":19079},{"id":19166,"depth":82,"text":19167},"2023-07-11","دالة مساعدة صغيرة غير متزامنة تستخدم كائن Image في المتصفح لتخبرك ما إذا كان الرابط ينتهي فعلاً إلى صورة قابلة للتحميل، بلا fetch وبلا صداع CORS.",[19333,19334,19335,13790,19336,19337,19338,1846],"JavaScript","التحقق من الصور","غير متزامن","صورة احتياطية","كائن Image","Browser API",{},"\u002Fwrites-ar\u002Fvalidating-image-urls-without-complex-error-handling",{"title":18965,"description":19331},"writes-ar\u002Fvalidating-image-urls-without-complex-error-handling","XlV8SlQT9SIiyjkkieuoCXTfJns_RiE1EzW2ZT7t-Vk",1793901268444]