فهرست پروازیRail TOC

فهرستِ مطالبِ کنارِ صفحه با یک ریلِ نقطه‌چین و یک هواپیمای کاغذی که با اسکرول رویش سفر می‌کند: نرم جلو می‌رود، روی زیرسرفصل‌ها می‌پیچد و اگر برگردید می‌چرخد و برمی‌گردد.

محمدجواد

ساخته شده توسط محمدجواد

سازنده فارسی‌فای، بچه دولوپر

وابستگی‌ها

motion

نوع تعامل

با اسکرولِ صفحه (یا ظرفی که با `containerRef` می‌دهید) هواپیما روی ریل جلو می‌رود و بخشِ پیموده‌شده پر می‌شود؛ هر سرفصلی که به خطِ دید برسد، ردیفش فعال و پررنگ می‌شود و `aria-current` هم می‌گیرد. روی هر ردیف بزنید تا صفحه تا همان سرفصل روان اسکرول شود؛ تا اولین اسکرولِ شما هواپیما روی همان ردیف می‌ماند، چون سرفصل‌های نزدیکِ پایان تا لنگرِ بالا نمی‌آیند. اگر به سمتِ بالا برگردید، هواپیما سرِ ریل می‌چرخد و رو‌به‌بالا برمی‌گردد — و با `prefers-reduced-motion` سفر و چرخش حذف می‌شود و هواپیما بی‌حرکت جابه‌جا می‌شود.

پراپ‌ها

گزینه‌هایی که می‌تونی به کامپوننت بدی؛ ستاره‌دارها اجباری‌اند.

items*
RailTocItem[]پیش‌فرض: [{ id: "intro", label: "مقدمه" }, { id: "install", label: "نصب", depth: 1 }]

سرفصل‌های صفحه. هر آیتم `{ id, label, depth }` است: `id` باید عیناً شناسهٔ همان سرفصل در DOM باشد، و `depth` (۰ به بعد) تورفتگیِ زیرسرفصل را می‌سازد.

containerRef
React.RefObject<HTMLElement | null>

ظرفِ اسکرولِ محتوا. ندهید تا ارتفاع و اسکرول از خودِ پنجره خوانده شود؛ در قابِ اسکرول‌شونده (مثلِ پنلِ مستندات) همین را بدهید و سرفصل‌ها باید داخلِ همان ظرف باشند.

offset
numberپیش‌فرض: 96

فاصلهٔ «خطِ دید» از بالای ظرف، پیکسل. یعنی سرفصلی که به این ارتفاع می‌رسد فعال می‌شود؛ مقدارِ کمتر یعنی زودتر فعال شدن.

indent
numberپیش‌فرض: 14

تورفتگیِ هر پله عمق، پیکسل. صفر یعنی سلسله‌مراتبِ تخت و ریلِ بی‌پیچ.

planeColor
stringپیش‌فرض: currentColor

رنگِ هواپیما — همان چیزی که روی ریل بالا و پایین می‌رود. هر مقدارِ CSS قبول است؛ ندهید تا رنگِ متنِ خودش را بگیرد (رفتارِ مرجع).

title
stringپیش‌فرض: "در این صفحه"

عنوانِ بالای فهرست. هم‌زمان `aria-label` ناوبری هم می‌شود.

نصب

۱. این آدرس رو یک‌بار توی components.json بذار تا shadcn مخزن فارسی‌فای رو بشناسه.

components.jsonjson
{
  "registries": {
    "@farsify": "https://farsify.ir/r/{name}.json"
  }
}

۲. بعد با دستورِ زیر کامپوننت رو نصب کن.

npx shadcn@latest add @farsify/rail-toc

پیش‌نیاز: Tailwind CSS v4 و صفحهٔ راست‌به‌چپ dir="rtl".

نمونه

rail-toc.tsxtsx
"use client";

import { useRef } from "react";
import { RailToc } from "@/components/farsify/rail-toc";

const SECTIONS = [
  { id: "intro", label: "مقدمه" },
  { id: "install", label: "نصب" },
  { id: "config", label: "پیکربندی" },
  { id: "theme", label: "رنگ و تم", depth: 1 },
  { id: "faq", label: "پرسش‌های پرتکرار" },
];

export function ArticleToc() {
  return <RailToc items={SECTIONS} />;
}

// خطِ دید پایین‌تر، زیرسرفصل‌های عمیق‌تر و عنوانِ دلخواه
<RailToc items={SECTIONS} offset={140} indent={22} title="فهرستِ این راهنما" />

// رنگِ هواپیما — تنها رنگِ تزئینیِ کامپوننت
<RailToc items={SECTIONS} planeColor="#0a84ff" />

// داخلِ پنلی که خودش اسکرول می‌شود — ظرف را بدهید تا اندازه از همان خوانده شود
const scrollerRef = useRef<HTMLDivElement>(null);
<RailToc items={SECTIONS} containerRef={scrollerRef} />

نکته‌ها

  • ریل در راست‌به‌چپ کنارِ لبهٔ راست (سمتِ شروع) می‌نشیند و تورفتگی به سمتِ چپ می‌رود؛ آینه‌سازی روی یک لایهٔ تزئینیِ `-scale-x-100` انجام می‌شود، پس هیچ مختصاتی را دستی نگردانید و خودِ فهرست هم با `dir` هر دو جهت کار می‌کند.
  • `id` هر آیتم باید همان شناسهٔ سرفصلِ واقعی در صفحه باشد؛ آیتمی که عنصرش پیدا نشود در محاسبهٔ پیشرفت نادیده گرفته می‌شود و هواپیما رویش نمی‌ایستد.
  • برای صفحه‌ای که خودش اسکرول می‌شود (نه پنجره) `containerRef` را بدهید و مطمئن شوید ظرف `position: relative` یا هر لنگرِ هندسیِ دیگری که لازم دارید را دارد؛ اندازه‌گیری با `offsetTop`ِ ردیف‌های فهرست انجام می‌شود و به اسکرولِ ظرف کاری ندارد.
  • پهنای ستون را خودِ کامپوننت نمی‌سازد؛ با `className="w-56"` یا هر عرضی که به متنِ فارسی‌تان جا بدهد ببندید تا برچسب‌های بلند سالم بشکنند.
  • با `prefers-reduced-motion` سفرِ فنری و چرخش حذف می‌شوند؛ فهرست، پر شدنِ ریل و `aria-current` سرِ جایشان می‌مانند.
  • `planeColor` تنها رنگِ کامپوننت است و روی خودِ هواپیما می‌نشیند، نه ریل؛ ریل از رنگِ متنِ تم ساخته می‌شود تا هم‌نشینیِ دو رنگ در یک ستون شلوغ نشود. برای هواپیمای برند، همان `var(--brand)`ِ پروژه‌تان را بدهید.

مجوز

کد بعد از نصب مالِ خودته؛ هر تغییری خواستی بده و توی پروژه‌ی خودت نگهش دار. برای پروژه‌های شخصی و تجاری آزاده، حتی اگه کدت بسته باشه. فقط اسم و لینک فارسی‌فای رو یه جا توی پروژه نگه دار.