قالب صفحهٔ تنظیمات (SettingsPage)

تنظیمات در گروه‌های مستقل — هر گروه یک بخش با عنوان، ردیف‌های برچسب‌کنار‌کنترل و نوار ذخیرهٔ خودش

معرفی

SettingsPage قالب صفحه‌ای است که تنظیمات را در چند گروه مستقل نشان می‌دهد؛ الگوی تنظیمات سوپابیس، برای راست‌به‌چپ آینه‌شده. عرض باریک 768 پیکسل است و هر گروه یک SettingsSection است:

  • عنوان و توضیح گروه؛
  • ردیف‌ها (FormRow) در یک کارت، برچسب کنار کنترل (از عرض md: ستون برچسب یک‌سوم، ستون کنترل دوسوم)، Switch و Checkbox روی خط برچسب در انتهای آن؛
  • یک نوار ذخیره برای هر بخش، پایین کارت همان بخش: نشانهٔ «تغییرات ذخیره نشده است» در ابتدای خط، «انصراف» و «ذخیره تغییرات» در انتهای آن.

قاعدهٔ ذخیره یکی است: هر بخش جدا ذخیره می‌شود، هیچ‌وقت یک دکمهٔ ذخیره برای کل صفحه. صفحه‌ای که یک فرم است و یک بار ثبت می‌شود FormPage است.

چه زمانی استفاده کنیم:

  • تنظیمات حساب، فضای کار یا محصول در چند گروه: اعلان‌ها، اعضا، اتصال‌ها.
  • هر گروهی که مستقل از بقیه ذخیره می‌شود.

چه زمانی استفاده نکنیم:

  • یک فرم با یک «ذخیره» (حتی اگر اسمش «تنظیمات هشدار» باشد): FormPage.
  • تغییری که فوراً اعمال می‌شود و ذخیره ندارد: همان کنترل در یک SettingsSection بدون onSubmit.

استفاده

هر بخش فرم react-hook-form خودش را دارد؛ formState.isDirty نوار ذخیره را روشن می‌کند و reset تغییرات را کنار می‌گذارد:

'use client'
import { useForm } from 'react-hook-form'
import { Input, Switch } from '@partodata/ui'
import { FormField } from '@partodata/ui/form'
import { FormRow, SettingsPage, SettingsSection } from '@partodata/ui/templates'

function EmailSettings({ save }: { save: (values: { weekly: boolean; address: string }) => Promise<void> }) {
  const form = useForm({ defaultValues: { weekly: true, address: 'team@example.com' } })
  return (
    <SettingsSection
      title="گزارش ایمیلی"
      description="خلاصهٔ دوره‌ای منشن‌ها که به ایمیل تیم می‌رسد"
      onSubmit={form.handleSubmit(async (values) => {
        await save(values)
        form.reset(values)
      })}
      onCancel={() => form.reset()}
      dirty={form.formState.isDirty}
      submitting={form.formState.isSubmitting}
    >
      <FormField
        control={form.control}
        name="weekly"
        render={({ field }) => (
          <FormRow label="گزارش هفتگی" description="هر شنبه صبح">
            <Switch checked={field.value} onCheckedChange={field.onChange} />
          </FormRow>
        )}
      />
      <FormField
        control={form.control}
        name="address"
        rules={{ required: 'نشانی ایمیل را وارد کنید' }}
        render={({ field, fieldState }) => (
          <FormRow label="نشانی ایمیل" required error={fieldState.error?.message}>
            <Input kind="email" {...field} />
          </FormRow>
        )}
      />
    </SettingsSection>
  )
}

export default function NotificationSettingsPage() {
  return (
    <SettingsPage title="تنظیمات اعلان‌ها" description="گزارش‌ها و هشدارهایی که دریافت می‌کنید">
      <EmailSettings save={async () => {}} />
    </SettingsPage>
  )
}

حالت‌ها و انواع

نوار ذخیره و نشانهٔ تغییر

dirtyدکمهٔ ذخیره«انصراف»نشانهٔ «تغییرات ذخیره نشده است»
falseغیرفعالپنهانپنهان
trueفعالپیداپیدا

onSubmit، dirty و onCancel با هم می‌آیند: بخشی که ذخیره می‌کند هر سه را دارد و بخشی که ذخیره نمی‌کند هیچ‌کدام از ویژگی‌های ذخیره را نمی‌گیرد (هر دو اشتباه خطای نوع‌اند). submitting دکمهٔ ذخیره را به حالت بارگذاری می‌برد و error خلاصهٔ خطای ذخیره را بالای کارت همان بخش نشان می‌دهد. اگر «انصراف» یا «ذخیره تغییرات» با صفحه‌کلید زده شود و از صفحه برود یا غیرفعال شود، فوکوس روی فرم همان بخش می‌ماند، نه <body>.

اقدام‌های ثانوی بخش

«تأیید توکن»، «تست اتصال» یا «بازگردانی پیش‌فرض‌ها»ی یک بخش secondaryActions همان SettingsSection است: در ابتدای نوار ذخیره، پیش از نشانهٔ تغییر، هر کدام Button variant="default" که بخش را ذخیره نمی‌کند. هر بخش ناحیهٔ مستقل خودش است: دکمهٔ ذخیره‌اش اقدام اصلی همان بخش است. بخشی که ذخیره نمی‌شود اقدام‌های ثانوی‌اش را تنها در پاورقی نشان می‌دهد.

بخش بدون ذخیره

SettingsSection بدون onSubmit فقط ردیف‌هایش را نشان می‌دهد، بی فرم و بی نوار ذخیره: برای اطلاعات فقط‌خواندنی یا دکمه‌ای مثل «حذف فضای کار» (با variant="destructive" و یک AlertDialog تأیید).

بارگذاری

state روی SettingsPage (با pageState({ data: settings, isLoading, error, onRetry })) همهٔ بخش‌ها را با اسکلت ردیف‌ها عوض می‌کند و سرِ صفحه می‌ماند؛ state روی یک SettingsSection فقط کارت همان بخش را. تنظیمات حالت خالی ندارند.

راهنمای استفاده

بکنید

  • برای هر گروه مستقل یک SettingsSection با فرم خودش بنویسید.
  • dirty و onCancel را از فرم همان بخش بدهید (formState.isDirty، () => form.reset()).
  • فیلدها را با FormRow مستقیم در بخش بنویسید؛ چیدمان کنار هم را بخش می‌دهد. گروه دیگری از تنظیمات، یک SettingsSection دیگر است — نه FormSection و نه Card داخل بخش (هر دو در حالت توسعه هشدار می‌دهند).

نکنید

  • یک دکمهٔ «ذخیره» برای کل صفحه نگذارید؛ هر بخش نوار خودش را دارد.
  • بخش را در Card خودتان نپیچید و برچسب را خودتان کنار کنترل نچینید.
  • برای تنظیمات زیرصفحه، زبانه در محتوا نسازید؛ زیرصفحه‌ها سطح دوم منوی ProductFrameاند (children آیتم منو).

Props

SettingsPage

Prop

Type

SettingsSection

Prop

Type

دسترسی‌پذیری

  • هر بخش یک نشانهٔ نام‌دار دارد، نه دو: بخشی که ذخیره می‌کند فرمی (form) به نام عنوان h2 خودش است، و بخش بی ذخیره یک ناحیهٔ نام‌دار (region).
  • دکمهٔ ذخیرهٔ غیرفعال یعنی چیزی برای ذخیره نیست؛ نشانهٔ تغییر متن دارد، نه فقط رنگ.
  • خلاصهٔ خطای ذخیره role="alert" دارد و فوکوس می‌گیرد.

کامپوننت‌های مرتبط

  • FormPage — یک فرم با یک ثبت.
  • FormRow — هر فیلد؛ در بخش تنظیمات برچسبش کنار کنترل است.
  • AlertDialog — تأیید کار برگشت‌ناپذیر یک بخش بدون ذخیره.