{"id":"l-css-4-2","courseId":"css-mastery","lang":"ar","kind":"lesson","summary":"حرّك الانتقال بين حالتين للصفحة نفسها باستخدام document.startViewTransition()، وأعطِ العناصر أسماء لتتحوّل بسلاسة إلى مواضعها الجديدة، وخصّص الحركة أو عطّلها في CSS.","html":"<p>حين يفعّل زائر Waypoint خيار «Hide sold-out»، تختفي ثلاث بطاقات وتقفز البقية إلى الأعلى لتملأ الفراغات. التغيير فوري وصحيح، لكنه مربك: كانت عينك على البطاقة الرابعة، والآن يقف شيء آخر مكانها. حركة سلسة من التخطيط القديم إلى الجديد كانت ستخبر العين إلى أين ذهبت الأشياء.</p>\n<p>تحريك تغييرات التخطيط كان يعني قياس كل عنصر قبل التغيير وبعده وتشغيل حركات FLIP في JavaScript. أما View Transition API فتقوم بالقياس نيابةً عنك.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">التقاط، تحديث، تحريك</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">filters.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> button</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#hide-full'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> applyFilter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> hide</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> button.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'aria-pressed'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'true'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  button.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'aria-pressed'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(hide));</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> card</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelectorAll</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'.session-card.is-full'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    card.hidden </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> hide;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">button.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'click'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">document.startViewTransition) {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    applyFilter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();          </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// older browsers: just update</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">startViewTransition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> applyFilter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">());</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>تؤدّي <code dir=\"ltr\">startViewTransition()</code> أربعة أشياء بالترتيب:</p>\n<ol>\n<li>تلتقط صورة (snapshot) للصفحة الحالية.</li>\n<li>تستدعي دالة التحديث لديك، التي تغيّر الـ DOM كيفما شاءت.</li>\n<li>تلتقط الحالة الجديدة.</li>\n<li>تبني شجرة من الـ pseudo-elements تعرض الصورتين القديمة والجديدة، وتحرّك الانتقال بينهما.</li>\n</ol>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 200\" role=\"img\" aria-labelledby=\"l-css-4-2-t1\"><defs><marker id=\"arrow-l-css-4-2-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-css-4-2-t1\">أربع خطوات في صف تصل بينها أسهم: التقاط الصورة القديمة، تشغيل callback التحديث، التقاط الصورة الجديدة، تحريك شجرة الـ pseudo-elements من القديم إلى الجديد.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"50\" width=\"150\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"30\" y=\"80\">1. التقاط</text>\n  <text class=\"d-label-muted\" x=\"30\" y=\"102\">الحالة القديمة</text>\n  <rect class=\"d-box-primary\" x=\"186\" y=\"50\" width=\"150\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"206\" y=\"80\">2. تحديث</text>\n  <text class=\"d-code\" x=\"206\" y=\"102\">applyFilter()</text>\n  <rect class=\"d-box\" x=\"362\" y=\"50\" width=\"150\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"382\" y=\"80\">3. التقاط</text>\n  <text class=\"d-label-muted\" x=\"382\" y=\"102\">الحالة الجديدة</text>\n  <rect class=\"d-box-accent\" x=\"538\" y=\"50\" width=\"150\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"558\" y=\"80\">4. تحريك</text>\n  <text class=\"d-code\" x=\"558\" y=\"102\">::view-transition</text>\n  <path class=\"d-arrow\" d=\"M160 85 L182 85\" marker-end=\"url(#arrow-l-css-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M336 85 L358 85\" marker-end=\"url(#arrow-l-css-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M512 85 L534 85\" marker-end=\"url(#arrow-l-css-4-2-1)\"/>\n  <text class=\"d-label-muted\" x=\"10\" y=\"170\">الـ DOM الحقيقي في حالته الجديدة أصلًا أثناء تشغيل الخطوة 4.</text>\n</svg><figcaption>يلتقط الـ view transition الحالة القديمة، ويشغّل التحديث، ويلتقط الحالة الجديدة، ثم يحرّك الانتقال</figcaption></figure>\n<p>الحركة الافتراضية تلاشٍ متقاطع للصفحة كلها. إنها خفيفة، وأفضل من القفزة بالفعل. تحديث الـ DOM نفسه حقيقي وفوري؛ الـ <em>صورة</em> وحدها هي التي تتحرّك، وأثناء تشغيلها تكون الصفحة تحتها في حالتها الجديدة أصلًا. وإن رمى التحديث خطأً أو تُخطّي الانتقال، يظل تغيير الـ DOM يحدث.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">سمِّ العناصر لتتحرّك</h2>\n<p>لتجعل كل بطاقة تنزلق إلى موضعها الجديد، أعطها <code dir=\"ltr\">view-transition-name</code>. كل عنصر مسمّى يحصل على زوج صور خاص به، ويحرّك المتصفّح موضعه وحجمه من القديم إلى الجديد:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  view-transition-name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--vt-name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Give every card a unique name once, when the list renders</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> card</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelectorAll</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'.session-card'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  card.style.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setProperty</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'--vt-name'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`card-${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">card</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>يجب أن تكون الأسماء <strong>فريدة</strong> بين العناصر المرسومة لحظة الالتقاط. البطاقات التي تختفي تتلاشى (فهي موجودة في الصورة القديمة وحدها)، والبطاقات الباقية تنزلق إلى الأعلى لتملأ الفراغات. وعينك تتبعها.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>إعادة استخدام اسم واحد لقائمة كاملة</span></p>\n<p><code dir=\"ltr\">.session-card { view-transition-name: card; }</code> تعطي كل البطاقات الاسم نفسه. ومع التكرار لا يستطيع المتصفّح مطابقة الصور، فيتخطّى الانتقال كله وتتغيّر الصفحة فورًا، مع خطأ في الـ console عن أسماء مكرّرة. ولّد اسمًا فريدًا لكل عنصر، من الـ id أو الترتيب.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">تنسيق الحركة</h2>\n<p>تعيش الحركة في شجرة من الـ pseudo-elements: <code dir=\"ltr\">::view-transition</code> في القمة، و<code dir=\"ltr\">::view-transition-group(name)</code> لكل عنصر مسمّى (إضافةً إلى <code dir=\"ltr\">root</code> لبقية الصفحة)، وداخل كلٍّ منها <code dir=\"ltr\">::view-transition-old(name)</code> و<code dir=\"ltr\">::view-transition-new(name)</code>. وتُحرَّك بحركات CSS عادية، فتتحكّم فيها بـ CSS عادية:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">::view-transition-group(</span><span style=\"color:#116329;--shiki-dark:#7EE787\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation-duration</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">250</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation-timing-function</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* Keep the header still while the list moves */</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.site-header</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">view-transition-name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: header; }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">::view-transition-group(</span><span style=\"color:#116329;--shiki-dark:#7EE787\">header</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">none</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span></code></pre></div>\n<p>الرمز <code dir=\"ltr\">*</code> يطابق كل المجموعات. ولتقليل الحركة، أزل الحركات ليكون التغيير فوريًا:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@media</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (prefers-reduced-motion: reduce) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ::view-transition-group(</span><span style=\"color:#116329;--shiki-dark:#7EE787\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ::view-transition-old(</span><span style=\"color:#116329;--shiki-dark:#7EE787\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ::view-transition-new(</span><span style=\"color:#116329;--shiki-dark:#7EE787\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">none</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> !important</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>لا توجد <code dir=\"ltr\">!important</code> هنا لتهزم حركات المتصفّح الافتراضية؛ فقواعدك العادية تتغلّب عليها أصلًا، كما رأيت في الدرس الأول. إنها موجودة ليتغلّب تفضيل المستخدم أيضًا على أيّ حركة أدقّ كتبتها أنت أو مكتبة ما لمجموعة مسمّاة بعينها، مثل <code dir=\"ltr\">::view-transition-group(header)</code>.</p>\n<p>وتستطيع أيضًا استهداف اتجاه واحد من التغيير. الصورة القديمة لبطاقة تختفي ليس لها إلا <code dir=\"ltr\">::view-transition-old(card-s2)</code>، دون نظير جديد، لذا فإن قاعدة مثل <code dir=\"ltr\">::view-transition-old(*) { animation-duration: 150ms; }</code> تجعل البطاقات المغادرة تتلاشى أسرع قليلًا من حركة البطاقات الباقية. وفروق صغيرة كهذه تجعل القائمة تبدو منظّمة لا مخلوطة.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">انتظار الانتقال</h2>\n<p>تُرجع <code dir=\"ltr\">startViewTransition()</code> كائن <code dir=\"ltr\">ViewTransition</code> فيه ثلاثة promises. <code dir=\"ltr\">updateCallbackDone</code> يُحسم حين يكتمل تحديثك للـ DOM، و<code dir=\"ltr\">ready</code> حين توشك الحركة على البدء، و<code dir=\"ltr\">finished</code> حين تنتهي وتزول الـ pseudo-elements. استخدم <code dir=\"ltr\">finished</code> لنقل الـ focus أو إعلان نتيجة بعد أن تستقرّ الحركة، مثل إرسال الـ focus إلى أول بطاقة باقية لمستخدمي لوحة المفاتيح. لكن لا تجعل أيّ شيء جوهري ينتظر الحركة: ففي متصفّح بلا هذه الـ API لا يوجد كائن انتقال أصلًا.</p>\n<aside class=\"callout callout-note\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">ملاحظة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الدعم</span></p>\n<p>أصبحت الـ view transitions داخل المستند نفسه ضمن Baseline (متاحة حديثًا) في أكتوبر 2025، حين أطلقها Firefox 144. أما الانتقالات عبر المستندات بين صفحات موقع متعدّد الصفحات (وتُفعَّل بـ <code dir=\"ltr\">@view-transition { navigation: auto; }</code>) فتعمل في Chromium وSafari لكن ليس في Firefox بعد، لذا تعامل معها على أنها تحسين. وفي الحالتين، المتصفّح الذي لا يدعمها يحدّث الصفحة فحسب، ولهذا يهمّ فحص الميزة في معالج النقر.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">دورك الآن</h2>\n<p>زر الفلتر في التمرين يخفي الجلسات الممتلئة دون حركة. أعطِ كل بطاقة اسمًا فريدًا، وغلّف التحديث في view transition مع بديل احتياطي، واضبط مدة المجموعات على 250ms، وأوقف الحركة لمن يفضّلون تقليلها. تراقب الفحوصات <code dir=\"ltr\">startViewTransition</code> بـ spy وتحذفها أيضًا لتتأكّد من أن بديلك الاحتياطي يعمل. وبعدها ستربط تقدّم الحركة بالتمرير بدل الزمن.</p>\n","toc":[{"id":"sec-1","text":"التقاط، تحديث، تحريك","level":2},{"id":"sec-2","text":"سمِّ العناصر لتتحرّك","level":2},{"id":"sec-3","text":"تنسيق الحركة","level":2},{"id":"sec-4","text":"انتظار الانتقال","level":2},{"id":"sec-5","text":"دورك الآن","level":2}],"takeaways":["<code dir=\"ltr\">document.startViewTransition(update)</code> تلتقط صورة للصفحة، وتشغّل تحديثك للـ DOM، وتلتقط صورة ثانية، ثم تحرّك الانتقال بينهما بتلاشٍ متقاطع (crossfade) افتراضيًا.","العنصر الذي له <code dir=\"ltr\">view-transition-name</code> فريد يحصل على زوج صور خاص به، فيتحرّك ويتغيّر حجمه بسلاسة إلى موضعه الجديد بدل أن يتلاشى.","الحركة مبنية من pseudo-elements مثل <code dir=\"ltr\">::view-transition-group(name)</code>، تنسّقها بحركات CSS عادية.","افحص دائمًا وجود <code dir=\"ltr\">startViewTransition</code> وشغّل التحديث مباشرةً حين تكون غائبة، وأزل الحركة للمستخدمين الذين يفضّلون تقليل الحركة."],"further":[{"title":"View Transition API (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API"},{"title":"Document: startViewTransition() method (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition"},{"title":"Same-document view transitions have become Baseline Newly available (web.dev)","url":"https://web.dev/blog/same-document-view-transitions-are-now-baseline-newly-available"}],"quiz":[{"q":"<p>استدعيت <code dir=\"ltr\">document.startViewTransition(() =&gt; applyFilter())</code> دون أيّ CSS على الإطلاق. ماذا يرى المستخدم؟</p>","options":[{"text":"لا شيء؛ الـ view transitions تحتاج إلى <code dir=\"ltr\">view-transition-name</code> قبل أن تتحرّك.","why":"دون أسماء تُلتقط الصفحة كلها على أنها <code dir=\"ltr\">root</code> وتتحرّك رغم ذلك."},{"text":"يُطبَّق الفلتر فورًا، لأن مدة الحركة الافتراضية صفر.","why":"الافتراضي تلاشٍ متقاطع قصير، لا تغيّر فوري."},{"text":"تلاشيًا متقاطعًا سريعًا للصفحة كلها من الحالة القديمة إلى الجديدة.","why":"صحيح. زوج صور الـ root يتلاشى تقاطعيًا افتراضيًا؛ والأسماء تضيف حركة لكل عنصر فوق ذلك."}],"answer":2},{"q":"<p>لبطاقتين في الصفحة <code dir=\"ltr\">view-transition-name: card</code> كلتيهما. ماذا يحدث حين تبدأ انتقالًا؟</p>","options":[{"text":"يُتخطّى الانتقال، لأن الأسماء يجب أن تكون فريدة بين العناصر المرسومة.","why":"صحيح. الأسماء المكرّرة تجعل المتصفّح يُلغي الحركة؛ لكن تحديث الـ DOM يظل يعمل، فتتغيّر الصفحة فورًا فحسب."},{"text":"تتحرّك البطاقتان معًا كمجموعة واحدة.","why":"للمجموعة صورة قديمة واحدة وصورة جديدة واحدة بالضبط؛ ولا يستطيع المتصفّح دمج عنصرين في واحد."},{"text":"تتحرّك البطاقة الأولى في الـ DOM وحدها.","why":"المتصفّح لا يختار فائزًا؛ التكرار يُبطل الانتقال كله."},{"text":"تحصل البطاقة الثانية على اسم مولَّد تلقائيًا.","why":"مع اسم صريح مكرّر لا يُولَّد شيء؛ ويُتخطّى الانتقال."}],"answer":0},{"q":"<p>كيف تجعل كل view transition في الصفحة تستغرق 250ms؟</p>","options":[{"text":"<code dir=\"ltr\">document.startViewTransition({ duration: 250 })</code>","why":"الدالة تأخذ callback للتحديث (أو كائن خيارات فيه <code dir=\"ltr\">update</code> و<code dir=\"ltr\">types</code>)، لا مدة."},{"text":"<code dir=\"ltr\">::view-transition-group(*) { animation-duration: 250ms; }</code>","why":"صحيح. المجموعات تشغّل حركات CSS، فتتحكّم خصائص الحركة العادية في توقيتها."},{"text":"<code dir=\"ltr\">view-transition-duration: 250ms</code> على <code dir=\"ltr\">:root</code>","why":"لا توجد خاصية كهذه؛ التوقيت يأتي من الحركات المطبّقة على الـ pseudo-elements."}],"answer":1},{"q":"<p>أيّ عبارة عن الدعم دقيقة في 2026؟</p>","options":[{"text":"الـ view transitions داخل المستند نفسه وعبر المستندات كلتاهما تعملان في كل متصفّح رئيسي.","why":"الـ view transitions عبر المستندات للتطبيقات متعدّدة الصفحات غير متاحة في Firefox بعد، لذا فهي ليست ضمن Baseline."},{"text":"الـ view transitions لا تعمل إلا في متصفّحات Chromium.","why":"Safari وFirefox كلاهما يدعمان الآن الـ view transitions داخل المستند نفسه."},{"text":"الـ view transitions تحتاج إلى router من إطار عمل لتعمل.","why":"إنها API في المتصفّح؛ وأيّ تحديث للـ DOM داخل الـ callback يعمل، بإطار عمل أو دونه."},{"text":"الـ view transitions داخل المستند نفسه ضمن Baseline منذ أكتوبر 2025؛ أما عبر المستندات فليست ضمن Baseline بعد.","why":"صحيح. أكمل Firefox 144 دعم الانتقالات داخل المستند نفسه؛ أما انتقالات MPA (عبر المستندات) فما زالت تفتقر إلى دعم Firefox."}],"answer":3}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<button id=\"hide-full\" type=\"button\" aria-pressed=\"false\">Hide sold-out</button>\n<section class=\"schedule\">\n  <article class=\"session-card\" id=\"s1\">Shipping View Transitions</article>\n  <article class=\"session-card is-full\" id=\"s2\">Grid Layout Workshop · Sold out</article>\n  <article class=\"session-card\" id=\"s3\">Color Spaces for Humans</article>\n  <article class=\"session-card is-full\" id=\"s4\">Container Queries Lab · Sold out</article>\n  <article class=\"session-card\" id=\"s5\">The Cost of a Frame</article>\n</section>","css":".schedule { display: grid; gap: 8px; margin-top: 12px; }\n.session-card {\n  padding: 10px 14px;\n  border: 1px solid #e5e7eb;\n  border-radius: 10px;\n  background: white;\n}\n.session-card[hidden] { display: none; }\n","js":"const button = document.querySelector('#hide-full');\n\nfunction applyFilter() {\n  const hide = button.getAttribute('aria-pressed') !== 'true';\n  button.setAttribute('aria-pressed', String(hide));\n  for (const card of document.querySelectorAll('.session-card.is-full')) {\n    card.hidden = hide;\n  }\n}\n\nbutton.addEventListener('click', () => {\n  applyFilter();\n});\n"},"solution":{"html":"<button id=\"hide-full\" type=\"button\" aria-pressed=\"false\">Hide sold-out</button>\n<section class=\"schedule\">\n  <article class=\"session-card\" id=\"s1\">Shipping View Transitions</article>\n  <article class=\"session-card is-full\" id=\"s2\">Grid Layout Workshop · Sold out</article>\n  <article class=\"session-card\" id=\"s3\">Color Spaces for Humans</article>\n  <article class=\"session-card is-full\" id=\"s4\">Container Queries Lab · Sold out</article>\n  <article class=\"session-card\" id=\"s5\">The Cost of a Frame</article>\n</section>","css":".schedule { display: grid; gap: 8px; margin-top: 12px; }\n.session-card {\n  padding: 10px 14px;\n  border: 1px solid #e5e7eb;\n  border-radius: 10px;\n  background: white;\n}\n.session-card[hidden] { display: none; }\n.session-card {\n  view-transition-name: var(--vt-name);\n}\n\n::view-transition-group(*) {\n  animation-duration: 250ms;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  ::view-transition-group(*),\n  ::view-transition-old(*),\n  ::view-transition-new(*) {\n    animation: none !important;\n  }\n}\n","js":"const button = document.querySelector('#hide-full');\n\n// A unique view-transition-name per card\nfor (const card of document.querySelectorAll('.session-card')) {\n  card.style.setProperty('--vt-name', `card-${card.id}`);\n}\n\nfunction applyFilter() {\n  const hide = button.getAttribute('aria-pressed') !== 'true';\n  button.setAttribute('aria-pressed', String(hide));\n  for (const card of document.querySelectorAll('.session-card.is-full')) {\n    card.hidden = hide;\n  }\n}\n\nbutton.addEventListener('click', () => {\n  if (!document.startViewTransition) {\n    applyFilter();\n    return;\n  }\n  document.startViewTransition(() => applyFilter());\n});\n"},"checks":[{"id":"unique-names","test":"const names = $$('.session-card').map((c) => style(c, 'view-transition-name')); return names.every((n) => n && n !== 'none' && n !== 'auto') && new Set(names).size === names.length;"},{"id":"uses-view-transition","test":"const waitFor = async (fn) => { for (let i = 0; i < 60; i++) { if (fn()) return true; await tick(25); } return fn(); }; if (!document.startViewTransition) return false; const orig = document.startViewTransition; let called = false; document.startViewTransition = function (...args) { called = true; return orig.apply(document, args); }; try { await click('#hide-full'); await waitFor(() => $('#s2').hidden); } finally { document.startViewTransition = orig; } return called;"},{"id":"filter-works","test":"const waitFor = async (fn) => { for (let i = 0; i < 60; i++) { if (fn()) return true; await tick(25); } return fn(); }; return (await waitFor(() => $('#s2').hidden && $('#s4').hidden)) && !$('#s1').hidden && $('#hide-full').getAttribute('aria-pressed') === 'true';"},{"id":"fallback-works","test":"const waitFor = async (fn) => { for (let i = 0; i < 60; i++) { if (fn()) return true; await tick(25); } return fn(); }; const orig = document.startViewTransition; document.startViewTransition = undefined; let ok = false; try { await click('#hide-full'); ok = await waitFor(() => !$('#s2').hidden); } catch (e) { ok = false; } finally { document.startViewTransition = orig; } return ok;"},{"id":"group-duration","test":"const all = []; const walk = (rules, media) => { for (const r of rules) { all.push({ r, media }); if (r.cssRules) walk(r.cssRules, media || (r instanceof CSSMediaRule && r.conditionText.includes('prefers-reduced-motion'))); } }; for (const s of document.styleSheets) walk(s.cssRules, false); return all.some(({ r }) => r.selectorText && r.selectorText.includes('::view-transition-group') && (parseFloat(r.style.animationDuration) === 250 || parseFloat(r.style.animationDuration) === 0.25));"},{"id":"reduced-motion","test":"const all = []; const walk = (rules, media) => { for (const r of rules) { all.push({ r, media }); if (r.cssRules) walk(r.cssRules, media || (r instanceof CSSMediaRule && r.conditionText.includes('prefers-reduced-motion'))); } }; for (const s of document.styleSheets) walk(s.cssRules, false); return all.some(({ r, media }) => media && r.selectorText && r.selectorText.includes('::view-transition') && (r.style.animation || r.style.animationName));"}],"title":"حرّك الفلتر بـ view transition","prompt":"<p>النقر على <strong>Hide sold-out</strong> يخفي بطاقتين فورًا. اجعل التغيير متحرّكًا:</p>\n<ol>\n<li>أعطِ كل <code dir=\"ltr\">.session-card</code> قيمة <code dir=\"ltr\">view-transition-name</code> <strong>فريدة</strong> (في CSS أو JS أو كليهما).</li>\n<li>شغّل تحديث الفلتر داخل <code dir=\"ltr\">document.startViewTransition()</code>، وارجع إلى تحديث عادي حين لا تكون الدالة موجودة.</li>\n<li>اجعل كل حركة <code dir=\"ltr\">::view-transition-group</code> تستغرق <strong>250ms</strong>.</li>\n<li>أوقف حركات الـ view transition للمستخدمين الذين يفضّلون تقليل الحركة.</li>\n</ol>\n<p>تراقب الفحوصات <code dir=\"ltr\">document.startViewTransition</code> بـ spy وتحذفها أيضًا لتختبر بديلك الاحتياطي.</p>\n","hints":["قاعدة CSS واحدة تستطيع قراءة custom property خاصة بكل بطاقة: <code dir=\"ltr\">view-transition-name: var(--vt-name);</code>. اضبط <code dir=\"ltr\">--vt-name</code> لكل بطاقة من الـ id الخاص بها في JS.","في معالج النقر: <code dir=\"ltr\">if (!document.startViewTransition) { applyFilter(); return; }</code> ثم <code dir=\"ltr\">document.startViewTransition(() =&gt; applyFilter());</code>.","<code dir=\"ltr\">::view-transition-group(*) { animation-duration: 250ms; }</code>، وداخل <code dir=\"ltr\">@media (prefers-reduced-motion: reduce)</code> اضبط <code dir=\"ltr\">animation: none !important</code> على الـ pseudo-elements الخاصة بالمجموعة والقديم والجديد."],"explanation":"<p>لكل بطاقة الآن اسم فريد (<code dir=\"ltr\">card-s1</code> و<code dir=\"ltr\">card-s2</code>…)، فيطابق المتصفّح صورتيها القديمة والجديدة، ويُزلق البطاقات الباقية إلى الفراغات بينما تتلاشى المخفية. والاسم المشترك كان سيجعل المتصفّح يتخطّى الانتقال. وتغليف <code dir=\"ltr\">applyFilter()</code> في <code dir=\"ltr\">startViewTransition()</code> يعطي المتصفّح حالتي ما قبل وما بعد؛ وفحص الميزة يُبقي الزر عاملًا في المتصفّحات التي لا تملك الـ API. وحركات المجموعات حركات CSS عادية، فتضبط <code dir=\"ltr\">animation-duration</code> توقيتها، و<code dir=\"ltr\">animation: none</code> داخل استعلام تقليل الحركة تحوّلها إلى تغيّر فوري.</p>\n","checkLabels":{"unique-names":"لكل بطاقة view-transition-name فريد","uses-view-transition":"النقر يشغّل التحديث داخل startViewTransition()","filter-works":"تُخفى البطاقات الممتلئة وتتحدّث حالة الزر","fallback-works":"الزر يظل يعمل دون View Transition API","group-duration":"مجموعات الـ view transition تستغرق 250ms","reduced-motion":"تُزال الحركة عند تفضيل تقليل الحركة"}},"words":602,"readMinutes":4}