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.
- باز شدن: فرزند اسلات (صفحه intercept شده) mount میشود →
- RTL: از
useI18n().locale+localeDirections→data-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 بدون استفاده) دست نمیخورد.
تست و راستیآزمایی
npm run test(vitest) — تستهای موجود نباید بشکنند.npm run lint(biome).- دستی با dev server روی پورت 3001:
/en/questions-list→ کلیک سکشن: اسلاید از راست؛/fa/questions-list→ اسلاید از چپ.- بستن با دکمه close (flush)، back مرورگر، و شبیهسازی هاردور بک — همه با انیمیشن خروج.
- refresh مستقیم روی
/en/questions-list/personal_identity→ صفحه کامل. - حفظ اسکرول لیست پس از بستن؛ قفل اسکرول پشت پنل هنگام باز بودن.