You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

7.5 KiB

انیمیشن اسلاید صفحه جزئیات سکشن روی لیست سکشن‌ها (مطابق حسینیه‌اپ)

خلاصه تحقیق

حسینیه‌اپ (مرجع): پنل مداح URL-driven است (?panel=provider:id) و صفحه لیست هرگز unmount نمی‌شود؛ پنل با CSS خالص (بدون کتابخانه) با translateX(±100%) → 0 و زمان‌بندی 0.28s / cubic-bezier(0.32, 0.72, 0, 1) اسلاید می‌شود. جهت با side فیزیکی انتخاب می‌شود: اپ RTL → پنل از چپ می‌آید (قرینه سایدبار راست). الگوی PanelSlot محتوای پنل را تا پایان انیمیشن خروج mount نگه می‌دارد.

اپ مریج: Next.js 16.2.10 App Router، بدون هیچ کتابخانه انیمیشن. تمام مسیرهای بستن صفحه جزئیات از نوع router.replace(questionsListHref) هستند (۳ دکمه هدر، دکمه خروج با flush، هاردور بک) و دکمه close حالت تست به‌صورت پیش‌فرض router.back() می‌زند — یعنی طراحی باید مثل حسینیه «URL-driven با retention» باشد تا همه مسیرها خودکار انیمیت شوند، بدون دستکاری تک‌تک call-siteها.

معماری انتخابی (طبق انتخاب شما): Parallel Route @modal + Intercepting Route (.)[slug] + هاست retention — الگوی رسمی modal مستندات همین نسخه Next (node_modules/next/dist/docs/.../parallel-routes.md و intercepting-routes.md، هر دو verify شد).

ساختار فایل‌ها

۱. فایل‌های جدید (تماماً additive)

src/app/[lang]/questions-list/
├── layout.tsx                      ← رندر {children} + {modal} (server)
└── @modal/
    ├── layout.tsx                  ← wrapper نازک → SectionOverlayHost
    ├── default.tsx                 ← return null (الگوی رسمی؛ جلوگیری از 404 در hard-load)
    └── (.)[slug]/
        └── page.tsx                ← export { default } from "@/app/questions-list/[slug]/page"
src/components/Componentes/section-overlay-host.tsx   ← "use client" — قلب مکانیزم

۲. SectionOverlayHost (ترجمه‌ی PanelSlot حسینیه به Next App Router)

  • در @modal/layout.ts رندر می‌شود؛ چون layout اسلات در ناوبری‌های soft زنده می‌ماند، state آن پایدار است.
  • State machine: phase: 'enter' | 'open' | 'closing' | 'closed'
    • باز شدن: فرزند اسلات (صفحه intercept شده) mount می‌شود → SectionOverlayContext که host ارائه می‌دهد با markActive() از داخل QuestionDetailClient صدا زده می‌شود → host پنل را با کلاس off-screen رندر کرده و با requestAnimationFrame کلاس open اضافه می‌کند → CSS transition اسلاید ورود.
    • بسته شدن (هر مسیری: replace / back / هاردور): children اسلات به default (null) تغییر می‌کند → host عنصر قبلی را در state نگه می‌دارد (retention) و کلاس closing می‌دهد → ۲۹۰ms بعد unmount واقعی. دقیقاً همان activeDescriptor/mounted در PanelSlot.
  • RTL: از useI18n().locale + localeDirectionsdata-dir روی پنل. LTR از راست، RTL از چپ (قرینه حسینیه که در RTL از چپ می‌آید).
  • قفل اسکرول: کلاس section-overlay-open روی body (قرینه الگوی موجود body.dropdown-open .app-shell در globals.css خط ۲۳۲).
  • سایزینگ: position: fixed; inset-inline: 0; top/bottom: 0; margin-inline: auto + w-full sm:w-[375px] + padding-inline: 17px + padding-bottom: var(--safe-bottom) — دقیقاً قرینه .app-shell (body فلکس و وسط‌چین است، globals.css خط ۱۵۴) تا main با -mx-[17px] داخل پنل مثل قبل رفتار کند.

۳. تغییر در question-detail-client.tsx (حداقلی، ~۶ خط)

const overlay = useSectionOverlay(); // خارج از overlay → undefined → no-op
useEffect(() => overlay?.markActive(), [overlay]);

تست‌های موجود (question-detail-client.test.tsx) مستقیم رندر می‌کنند و context ندارند → بدون تغییر رفتار.

۴. CSS در globals.css

.section-overlay {
  transform: translateX(100%);                     /* LTR: ورود از راست */
  transition: transform 280ms cubic-bezier(0.32, 0.72, 0, 1);
  will-change: transform;
}
[dir="rtl"] .section-overlay { transform: translateX(-100%); }  /* RTL: ورود از چپ */
.section-overlay[data-open="true"], .section-overlay[data-open="closing"] { transform: translateX(0); }
/* closing = همان حالت 0 که با برداشتن data-open="true" به سمت ابتدایی برمی‌گردد */
body.section-overlay-open .app-shell { overflow-y: hidden; }
@media (prefers-reduced-motion: reduce) { .section-overlay { transition: none; } }
  • سایه لبه داخلی پنل مثل shadow-[-18px_0_50px_rgba(0,0,0,.45)] حسینیه (جهت سایه هم با RTL برعکس).

منحنی و مدت زمان عیناً از anim-sheet-left حسینیه (0.28s cubic-bezier(0.32, 0.72, 0, 1)).

رفتار نهایی

  • کلیک روی سکشن در /en/questions-list → URL به /en/questions-list/personal_identity تغییر می‌کند، لیست زیر پنل می‌ماند (اسکرول حفظ می‌شود)، پنل از راست (در /fa از چپ) با همان انیمیشن حسینیه اسلاید می‌شود.
  • هر مسیر بستن (دکمه close با flush پاسخ‌ها، back مرورگر، هاردور بک اندروید، اتمام سابمیت) → پنل با همان انیمیشن به همان سمت جمع می‌شود و لیست از زیر پیدا می‌شود.
  • بارگذاری مستقیم/refresh روی URL جزئیات → صفحه کامل فعلی بدون انیمیشن (مطابق رفتار حسینیه در hard-load).
  • متن جدیدی اضافه نمی‌شود → بدون تغییر فایل‌های locale.

نکات اجرا و ریسک

  • Dev با --webpack اجرا می‌شود؛ interception با webpack پشتیبانی می‌شود.
  • router.prefetch از لیست (که الان هم هست) نسخه intercept شده را prefetch می‌کند → ورود آنی.
  • اگر build روی static-params اسلات گیر کرد (بعید، همه force-dynamic هستند): export const dynamic = "force-dynamic" به صفحه intercept شده اضافه می‌شود.
  • کد مرده info-progress-card.tsx (لینک non-localized بدون استفاده) دست نمی‌خورد.

تست و راستی‌آزمایی

  1. npm run test (vitest) — تست‌های موجود نباید بشکنند.
  2. npm run lint (biome).
  3. دستی با dev server روی پورت 3001:
    • /en/questions-list → کلیک سکشن: اسلاید از راست؛ /fa/questions-list → اسلاید از چپ.
    • بستن با دکمه close (flush)، back مرورگر، و شبیه‌سازی هاردور بک — همه با انیمیشن خروج.
    • refresh مستقیم روی /en/questions-list/personal_identity → صفحه کامل.
    • حفظ اسکرول لیست پس از بستن؛ قفل اسکرول پشت پنل هنگام باز بودن.