انتخاب قالب صفحه

هر صفحهٔ محصول یک قالب است — فهرست، جزئیات، فرم، تنظیمات، داشبورد یا صفحهٔ کمکی — و قالب عرض، سرِ صفحه، جای اقدام‌ها و حالت‌ها را تعیین می‌کند

معرفی

هر صفحهٔ یک محصول پرتو با یک قالب ساخته می‌شود (@partodata/ui/templates). صفحه فقط جایگاه‌های قالب را پر می‌کند: عنوان، اقدام‌ها، فیلترها، داده و حالت داده. هر تصمیمی که دو توسعه‌دهنده یا دو مدل هوش مصنوعی ممکن است متفاوت بگیرند — عرض، فاصله‌ها، اندازهٔ عنوان، جای اقدام اصلی، شکل حالت بارگذاری و خالی و خطا، جای دکمه‌های فرم — در خود قالب است و عددی در صفحه نوشته نمی‌شود.

قالب‌ها درون ProductFrame رندر می‌شوند که یک بار در layout ریشهٔ محصول است؛ صفحه قاب نمی‌سازد.

نمونه بصری

پیاده‌سازی

جدول انتخاب

از بالا به پایین بخوانید؛ اولین ردیفی که درست است قالب صفحه است:

اگر صفحه…قالبعرض
پیش از ورود است: ورود، کد یک‌بارمصرف، بازیابی گذرواژه (بیرون از قاب)AuthPageکارت 400، در مرکز
404، 403، خطای کل صفحه، خالیِ اول‌کار یا در حال به‌روزرسانی استUtilityPageباریک (768)، در مرکز
کار اصلی‌اش یک پرسش است (جست‌وجوی پست، جست‌وجوی شواهد)ListPage با queryمثل فهرست
فهرستی از موجودیت‌ها است (با جست‌وجو و فیلتر یا بی آن‌ها)ListPageاز content: جدول 1200 (بیش از 8 ستون wide)، فید 680 (با aside 1200)، شبکه 1600
یک موجودیت را با بخش‌ها، زبانه‌ها یا ستون کناری نشان می‌دهدDetailPageپیش‌فرض (1200)؛ گزارش نمودار و جدول wide
یک چیز را در چند مرحله می‌سازد (منبع، سپس برچسب‌ها، سپس بازبینی)WizardPageباریک (768)
یک فرم است که یک بار ثبت می‌شود (ساخت، ویرایش، دعوت)FormPageباریک (768)
تنظیمات در چند گروه است که هر گروه جدا ذخیره می‌شودSettingsPageباریک (768)
شاخص و نمودار با بازهٔ زمانی داردDashboardPageپهن (1600) یا کامل
ارتفاع صفحه را پر می‌کند: پخش زنده کنار متن، نقشه، پنل‌های کنار هم، کنسولCustomPage با layout="fill" و PagePaneها (با DS-GAP)با نام
در هیچ‌کدام جا نمی‌شوداول به مسئول طراحی بگویید؛ تا تصمیم، CustomPage با DS-GAPبا نام

فرم یا تنظیمات؟ نام صفحه تعیین نمی‌کند، شمارِ دکمه‌های ذخیره تعیین می‌کند: یک «ذخیره» و یک «انصراف» برای کل صفحه ← FormPage (حتی اگر اسمش «تنظیمات هشدار» باشد)؛ چند گروه که هر کدام جدا ذخیره می‌شود ← SettingsPage.

دیالوگ، پنل کناری یا صفحه؟

اگر کار…جایش
کوتاه و متمرکز است: تأیید، فرم دو سه فیلدیDialog / AlertDialog
جزئیات یک ردیف یا فرم بلند است و کاربر نباید صفحهٔ جاری را ترک کندSheet (پنل کناری)
جزئیات ردیف‌هایی که کاربر پشت هم از فهرست بررسی می‌کند و پیوند مستقیم لازم دارد (کنسول عملیات)EntityDrawer + useEntityDrawer (موجودیت باز در نشانی، J/K)
یک جریان کامل است، یا باید نشانی (پیوند) داشته باشدیک صفحه با قالب

فرم دیالوگ و پنل کناری هم با FormRow ساخته می‌شود (در یک FormSection، برچسب بالا).

آنچه همهٔ قالب‌ها تعیین می‌کنند

  • یک h1: عنوان صفحه، 48 پیکسل زیر نوار بالای قاب. نوار بالای قاب عنوان صفحه ندارد. h1 دیگری در صفحه در محیط توسعه هشدار می‌دهد.
  • ترتیب و فاصله: سرِ صفحه ← (نوارابزار) ← محتوا. سرِ صفحه تا اولین بلوک 48، بخش تا بخش 48، بلوک تا بلوک 24، نوارابزار تا جدول 16 — همه از توکن‌های هندسهٔ صفحه. صفحهٔ فهرست کوتاه‌تر است: نوارابزارش 24 زیر سرِ صفحه، توضیح یک خط، و نوارابزار و سرِ جدول هنگام پیمایش می‌چسبند.
  • عرض از محتوا: جدول با نام، فید تک‌ستونی در اندازهٔ خواندن (680، content="feed"، با ستون کناری اختیاری)، شبکهٔ کارت پهن.
  • عرض: با نام (narrow 768 · default 1200 · wide 1600 · full)، هرگز عدد؛ قالب‌ها className و style نمی‌پذیرند.
  • اقدام‌ها: اقدام اصلی فقط یکی است (primaryAction، یک Button بدون variant که کاری می‌کند: onClick، یا اگر به صفحهٔ دیگری می‌رود <Button asChild><Link href>…</Link></Button>)؛ در انتهای سرِ صفحه، یا اگر صفحه ردیف فیلتر دارد در انتهای همان ردیف. اقدام‌های دیگر (secondaryActions) در همهٔ صفحه‌ها variant="default" کنار آن‌اند (خروجی جدول: DataTableExportButton، همیشه بی هیچ شرطی؛ قالب آن را برای فهرست بی‌ردیف غیرفعال با دلیلش نشان می‌دهد). در فرم، دکمهٔ ثبت اقدام اصلی است و در پایین کارت فرم. اقدام روی چند ردیف انتخاب‌شده، bulkActions در selection یک ListPage است: نوار انتخاب تا ردیفی انتخاب شده جای ردیف نوارابزار را می‌گیرد — هرگز در secondaryActions یا نواری از خود صفحه.
  • یک اقدام اصلی در هر ناحیهٔ مستقل: سرِ صفحه (با نوارابزارش)، یک PagePane، یک SettingsSection، یک EntityDrawer یا یک دیالوگ هر کدام ناحیهٔ خودشان‌اند و هر کدام یک اقدام اصلی دارند. دو تصمیم هم‌وزن (تأیید / رد) یک اقدام‌اند: DecisionActions در جای اقدام اصلی همان ناحیه؛ در صف بررسی emphasis="item".
  • دادهٔ زنده: صفحه‌ای که خودش تازه می‌شود حالتش را با pageState({ …, live }) می‌سازد (useLiveRefresh)؛ تازه‌شدن محتوا را دست نمی‌زند و شکستش داده را پاک نمی‌کند (دادهٔ زنده).
  • بازگشت: صفحه‌ای که مورد منوی خودش را دارد back ندارد؛ صفحه‌ای که از صفحهٔ دیگری باز می‌شود back به همان صفحه؛ breadcrumbs فقط دو سطح یا بیشتر عمیق — هرگز هر دو.
  • نما در نشانی: بازه، تب، فیلترها، مرتب‌سازی، چیدمان و شمارهٔ صفحه اگر باید با پیوند به اشتراک گذاشته شوند یا پس از «برگشت» بمانند، فقط با useViewParams در نشانی می‌روند: نه useSearchParams، نه useFilterParams/FilterProvider، نه history.replaceState (قاعدهٔ ESLint parto/page-template). دلیل: یک پاسخ برای همهٔ صفحه‌ها؛ نام پارامترها، شکل نوشتن آرایه و تاریخ، push یا replace و خواندن در اولین رندر را در آزمایش X5 سه اجرا به سه شکل ساختند، و تا یک راهِ DS نبود «نه» تنها پاسخ بود. متن تایپ‌شده، انتخاب و جای فید با مکان‌نما وضعیت جزء می‌مانند.
  • حالت‌ها: بارگذاری اسکلتی هم‌شکل محتواست؛ خطا ErrorState با «تلاش مجدد»؛ خالی Empty — هر سه به‌جای محتوا، هرگز داخل جدول یا کارت. سرِ صفحه هیچ‌وقت ناپدید نمی‌شود. حالت را همیشه با یک قاعده از خروجی درخواست بسازید: pageState({ data: result?.items, isLoading, error, onRetry }) — data خودِ فهرست (یا موجودیت) است، نه شیء صفحه‌ای که فهرست در آن است؛ در ListPage، filtered خود صفحه فهرست خالی را «نتیجه‌ای یافت نشد» با متن خود سیستم طراحی می‌کند. وقتی ردیف‌ها روی صفحه‌اند و صفحهٔ بعد یا نتیجهٔ فیلتر در راه است، ردیف‌ها کم‌رنگ می‌مانند؛ نشانهٔ بارگذاری دیگری لازم نیست. state در ListPage الزامی است.
  • عدم دسترسی: در سطح صفحه UtilityPage نوع 403، داخل قاب؛ در سطح یک اقدام، همان اقدام در جایگاه خودش، غیرفعال با دلیلش: GatedAction دور Button (<GatedAction allowed={canCreate} reason="…">) — هرگز پنهان (canCreate && <Button/>)، هرگز disabled بی دلیل. اقدامی که با ConfirmDialog تأیید می‌شود GatedAction را در trigger پنجره دارد، نه پنجره را درون GatedAction. گزینه‌ای از منوی عملیات یک ردیف که کاربر اجازه‌اش را ندارد از منو کنار گذاشته می‌شود (گزینهٔ غیرفعال منو نمی‌تواند دلیلش را بگوید).
  • کارت: تودرتو نیست. جدول، حالت خالی و نوارابزار داخل کارت نمی‌روند؛ فرم‌ها کارت خودشان را از قالب می‌گیرند.

الگوهای رایج

فهرست با جست‌وجو و فیلتر

<ListPage
  title="منشن‌ها"
  search={
    <SearchInput
      placeholder="جست‌وجو در منشن‌ها"
      aria-label="جست‌وجو در منشن‌ها"
      value={q}
      onChange={(e) => filterBy(setQ)(e.target.value)}
      onClear={() => filterBy(setQ)('')}
    />
  }
  filters={
    <>
      <DataTableFacetedFilter
        title="پلتفرم"
        options={platforms}
        selected={selected}
        onSelectedChange={filterBy(setSelected)}
      />
      <DateRangePicker value={range} onChange={filterBy(setRange)} placeholder="بازهٔ تاریخ" />
    </>
  }
  filtered={q !== '' || selected.length > 0 || range !== undefined}
  onClearFilters={clear}
  secondaryActions={<DataTableExportButton columns={columns} data={rows} filename="mentions.csv" label="خروجی CSV" />}
  primaryAction={
    <Button asChild iconStart={<Icons.plus />}>
      <Link href="/settings/alerts">ساخت هشدار</Link>
    </Button>
  }
  state={pageState({
    data: result?.items,
    isLoading,
    error,
    onRetry: reload,
    emptyCopy: { title: 'هنوز منشنی ثبت نشده است' },
  })}
  pagination={{ currentPage: page, totalPages, onPageChange: setPage, totalRows: result?.total ?? 0, pageSize: 25 }}
>
  <DataTable columns={columns} data={rows} />
</ListPage>

یک فرم

<Form {...form}>
  {/* «تنظیمات هشدار» مورد منوی خودش را دارد: بی back، و «انصراف» تغییرات را کنار می‌گذارد. */}
  <FormPage title="تنظیمات هشدار" onSubmit={form.handleSubmit(save)} onCancel={() => form.reset()} submitLabel="ذخیره">
    <FormField
      control={form.control}
      name="name"
      rules={{ required: 'نام هشدار را وارد کنید' }}
      render={({ field, fieldState }) => (
        <FormRow label="نام هشدار" required error={fieldState.error?.message}>
          <Input {...field} />
        </FormRow>
      )}
    />
  </FormPage>
</Form>

بخش تازه‌ای که هنوز خالی است

<UtilityPage
  kind="empty"
  title="هنوز گزارشی ساخته نشده است"
  description="گزارش‌ها خلاصهٔ دوره‌ای منشن‌ها هستند و پس از راه‌اندازی این بخش، این‌جا فهرست می‌شوند."
/>

عنوان وضعیت را می‌گوید، نه نام صفحه. action فقط وقتی هست که چیزی در محصول اولین مورد را بسازد (پیوند یا onClick به آن)؛ هرگز دکمه‌ای که هیچ کاری نمی‌کند.

نمونهٔ کامل هر قالب، با داده، مسیرها و حالت‌ها، در قالب شروع است: / داشبورد، /mentions فهرست، /mentions/[id] جزئیات با زبانه، /settings/alerts فرم، /reports صفحهٔ خالی و not-found صفحهٔ 404.

بهترین روش‌ها + دام‌های رایج

بهترین روش‌ها

  • پیش از نوشتن صفحه قالبش را از جدول بالا انتخاب کنید.
  • فقط جایگاه‌های قالب را پر کنید؛ اگر چیزی جا نداشت، DS-GAP ثبت کنید.
  • کنترل‌ها و دکمه‌های داخل قالب را بدون size و کلاس عرض بنویسید.

دام‌های رایج

  • ساختن صفحه از PageContainer و PageHeader دستی، یا قالب درون PageContainer: این‌ها اجزای سطح پایینی‌اند که قالب‌ها رویشان ساخته شده‌اند؛ صفحهٔ تازه با آن‌ها ساخته نمی‌شود و قالبِ درون PageContainer دو بار فاصله می‌گیرد. قاعدهٔ parto/page-template آن‌ها را در کد نشان می‌دهد و قالب درون PageContainer در محیط توسعه هشدار می‌دهد.
  • اقدام اصلی در سرِ صفحه در حالی که صفحه ردیف فیلتر دارد: primaryAction را به قالب بدهید تا جایش را خودش تعیین کند.
  • اقدام ثانوی outline یا ghost، یا اقدام اصلی بی onClick و بی پیوند: قاعدهٔ parto/page-primary-action هر دو را می‌گیرد.
  • Empty در emptyState جدول یا داخل کارت: حالت خالی را با state بدهید.
  • دکمهٔ ذخیرهٔ تمام‌عرض یا دکمه‌های دست‌ساز پایین فرم: دکمه‌ها را FormPage و SettingsSection می‌سازند.
  • FormPage برای تنظیمات گروه‌گروه، یا SettingsPage برای یک فرم: شمار دکمه‌های ذخیره تعیین می‌کند.

صفحات مرتبط

  • ProductFrame — قاب محصول که قالب‌ها درونش رندر می‌شوند.
  • PageState — حالت‌های بارگذاری، خطا و خالی.
  • FormRow — فیلد فرم در همهٔ قالب‌ها و دیالوگ‌ها.
  • هندسهٔ صفحه — توکن‌هایی که قالب‌ها از آن‌ها ساخته شده‌اند.
  • چیدمان — اجزای سطح پایین صفحه، برای وقتی که قالبی جا نمی‌شود.