پروایدر و دکمهٔ تعویض تم (ThemeProvider / ThemeToggle)
راهحل رسمی و یکپارچهٔ پرتو برای سوییچ زندهٔ تاریک/روشن — پیشفرض همیشه تاریک
معرفی
پرتو تاریکمحور است: مقادیر پایهٔ :root خودِ توکنها تاریک هستند (سلکتور بلوک تاریک :root, [data-theme='dark'], .dark است)، پس مصرفکنندهای که هیچ چیز اضافه نکند همین الان هم تاریک میبیند. تم روشن هم به همان اندازه کامل و تولیدی است — یک opt-in صریح زیر [data-theme='light'] / .light.
ThemeToggle دکمهای است که این سوییچ را در اختیار کاربر میگذارد، و ThemeProvider زیرساخت لازم برای کارکردنش (پیشفرض تاریک، ماندگاریِ انتخاب کاربر، و نوشتنِ همزمانِ کلاس .dark/.light و attribute data-theme که CSS سیستم طراحی هر دو را میخواند) را یکبار برای همیشه فراهم میکند.
چه زمانی استفاده کنیم:
- در هر محصولی که کاربر باید بتواند بین تم تاریک و روشن جابهجا شود (نه فقط توسعهدهنده در build-time).
- در اپی با
ProductFrameخودتانThemeToggleنمیگذارید: کافی استThemeProviderدر ریشه باشد؛ قاب کلید را در یک جای ثابت میگذارد (themeToggle="header"پیشفرض، آخرین کنترل نوار بالا پیش از منوی حساب؛ یا"user-menu"داخل منوی حساب). - خود
ThemeToggleفقط در سطحی بدون قاب: نوار بالای یک سایت معرفی یاSiteHeader، یک بار.
چه زمانی استفاده نکنیم:
- اگر محصول شما قطعاً و برای همیشه فقط یک تم دارد (بدون قصد دادن انتخاب به کاربر)، همان الگوی استاتیک قدیمی (
className="dark" data-theme="dark"بدون Provider) هنوز معتبر است — نصب یک peer dependency اضافه برای چیزی که هرگز عوض نمیشود لازم نیست. - اگر به یک سوییچ متنی دوحالته با لیبل (نه یک دکمهٔ آیکونی) نیاز دارید — مثلاً در یک صفحهٔ تنظیمات — مستقیماً از
useTheme()(صادرشدهٔ next-themes) باSwitchبسازید؛ThemeToggleبرای جای فشرده مثل هدر طراحی شده.
از subpath وارد کنید، نه barrel اصلی
چون next-themes یک peer dependency اختیاری در @partodata/ui است، این دو کامپوننت فقط از مسیر
@partodata/ui/theme-toggle در دسترساند، نه از @partodata/ui اصلی — تا محصولی که اصلاً سوییچ تم نمیخواهد مجبور به
نصب next-themes یا سنگینتر شدن barrel نشود.
راهاندازی (یکبار، در ریشهٔ اپ)
npm install next-themes// Next.js — app/layout.tsx
import { ThemeProvider } from '@partodata/ui/theme-toggle'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
// این دو marker همچنان لازماند: پیش از اجرای اسکریپت next-themes روی
// کلاینت، همینها جلوی «فلاش تمِ غلط» را میگیرند — و چون پیشفرضِ واقعی
// هم تاریک است، این یک حدسِ درست برای اکثریتِ قریببهاتفاقِ بازدیدهاست.
// suppressHydrationWarning: اسکریپت next-themes پیش از hydration هر دو
// marker را عوض میکند (توصیهٔ خودِ next-themes).
<html lang="fa" dir="rtl" className="dark" data-theme="dark" suppressHydrationWarning>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}// اپ Vite/CRA غیر-Next — main.tsx
import { ThemeProvider } from '@partodata/ui/theme-toggle'
document.documentElement.classList.add('dark')
document.documentElement.setAttribute('data-theme', 'dark')
createRoot(document.getElementById('root')!).render(
<ThemeProvider>
<App />
</ThemeProvider>
)next-themes فقط به react/react-dom وابسته است، نه به Next.js — همین provider بدون تغییر در هر دو نوع اپ کار میکند.
استفاده
import { ProductFrame } from '@partodata/ui/product-frame'
// ThemeProvider در ریشه؛ قاب کلید را خودش میگذارد. ThemeToggle در actions نه.
;<ProductFrame product={{ name: 'پایش برند' }} nav={nav} themeToggle="header">
{children}
</ProductFrame>ThemeToggle در actions قاب یا کلید دوم در همان فایل را قاعدهٔ lint parto/theme-toggle-placement گزارش میکند؛
قاب هم در محیط توسعه هشدار میدهد و فقط همان یکی را نگه میدارد.
حالتها و انواع
دکمهٔ پیشفرض
آیکونی و فشرده — همان نمونهٔ بالا. variant="ghost" پیشفرض است و بدون size مربعی 30 پیکسلی روی نردبان کنترلهاست (تا 3٫x دکمهٔ 36 پیکسلی icon)، دقیقاً مناسب یک اکشن هدر.
<ThemeToggle />با variant/size سفارشی
هر prop دیگری که Button میپذیرد پاس داده میشود:
<ThemeToggle variant="outline" size="md" className="border-border" />در نوار بالای ProductFrame
کلیدی که قاب ProductFrame میگذارد (و هر ThemeToggle بدون size در ردیفی با
ControlSizeProvider) دکمهای مربعی به ارتفاع همان ردیف است تا با بقیهٔ
دکمههای فقطآیکون همقد باشد. بیرون از چنین ردیفی مربعی 30 پیکسلی (sm) است؛ تا 3٫x دکمهٔ 36 پیکسلی icon بود.
برچسب سفارشی
نام دسترسپذیر دکمه نام همان حالتی است که روشن و خاموش میشود؛ aria-pressed میگوید روشن است یا نه.
<ThemeToggle label="حالت شب" />پیشفرض = تاریک، همیشه
ThemeProviderداخلاًnext-themesرا باdefaultTheme="dark"وenableSystem={false}صدا میزند — یعنی پرتو از ترجیح سیستمعامل کاربر پیروی نمیکند؛ همیشه با تاریک شروع میکند و کاربر آگاهانه به روشن سوییچ میکند، نه برعکس.- انتخاب کاربر با همان مکانیزم استاندارد next-themes (کلید
themeدرlocalStorage) بین بازدیدها حفظ میشود. - پیش از mount شدن روی کلاینت (لحظهای که SSR هنوز نمیداند کاربر قبلاً چه انتخابی کرده)،
ThemeToggleآیکون حالت تاریک را نشان میدهد، نه یک اسکلتون خالی — چون تاریک همان پیشفرض واقعی است، این حدس برای اکثریت بازدیدها درست است و از یک فلش/پرش بصری غیرضروری جلوگیری میکند. - CSS سیستم طراحی هم روی کلاس
.dark/.lightو هم روی attribute صریحdata-themeتعریف شده (بلوک توکنهای روشن[data-theme='light'], .lightاست و variantdark:با[data-theme='dark'] *هم فعال میشود)، پس این دو نباید حتی یک لحظه با هم ناسازگار باشند.ThemeProviderهر دو را از خودِ next-themes میخواهد (attribute={['class', 'data-theme']})، پس هر دو در یک لحظه نوشته میشوند: هم در اسکریپت مسدودکنندهٔ next-themes پیش از اولین رنگآمیزی، و هم در هر تعویض تم. لازم نیست خودتان effect جدایی برایش بنویسید. - تا پیش از نسخهٔ 4.0 فقط کلاس از next-themes میآمد و
data-themeدر یک effect ریاکت کپی میشد؛ بعد از بارگذاری دوبارهٔ صفحه در تم روشن، صفحه تا اجرای hydration باclass="light"وdata-theme="dark"رنگ میشد، یعنی نیمهروشن. اگر برای این مشکل provider محلی نوشتهاید، اکنون میتوانید آن را حذف کنید.
راهنمای استفاده
بکنید
ThemeToggleرا زیرThemeProviderرندر کنید، نه بیواسطه -ThemeProviderرا فقط یکبار، در ریشهٔ اپ قرار دهید - marker های استاتیکclassName="dark" data-theme="dark"را روی عنصر ریشه نگه دارید، حتی بعد از اضافهکردنThemeProvider— جلوی فلش تمِ غلط را میگیرند
نکنید
enableSystemرا رویtrueعوض نکنید — پرتو عمداً از ترجیح سیستمعامل پیروی نمیکند، همیشه با تاریک شروع میشود - چندThemeProviderتودرتو در یک اپ قرار ندهید — یک provider سراسری در ریشه کافی است -ThemeToggleرا از@partodata/uiاصلی import نکنید — فقط از subpath@partodata/ui/theme-toggleدر دسترس است
Props — ThemeToggle
هر prop دیگری که Button میپذیرد (بهجز onClick/children، که داخلی مدیریت میشوند) هم پاس داده میشود — از جمله variant (پیشفرض 'ghost')، size (پیشفرض 'sm'، مربع 30 پیکسلی؛ داخل ControlSizeProvider مربعی به ارتفاع ردیف) و className.
دسترسیپذیری
- عنصر زیرین یک
<button>واقعی است (از طریقButton) — باTabقابلدسترسی و باEnter/Spaceقابلفعالسازی. - یک دکمهٔ دوحالته است: نامش ثابت است («تم تاریک») و
aria-pressedمیگوید تم تاریک روشن است یا نه؛ صفحهخوان «تم تاریک، دکمهٔ دوحالته، فشرده» میخواند. آیکون تم فعلی را نشان میدهد (ماه در تاریک، خورشید در روشن). - نامی که عمل بعدی را بگوید («تغییر به تم روشن») با
aria-pressedجمع نمیشود: صفحهخوان هر دو را میخواند و حاصل برعکس تم فعلی شنیده میشود. برای همینdarkLabel/lightLabel(تا 4.x) در 5.0 حذف شدند.
تعامل با کیبورد
Tab: انتقال فوکوس به دکمه -EnterیاSpace: تعویض تم
کامپوننتهای مرتبط
- ProductFrame — کلید تم را خودش میگذارد (
themeToggle:headerیاuser-menu)؛ هرگزThemeToggleدرactions. - Switch — اگر بهجای دکمهٔ آیکونی یک سوییچ متنی/دوحالته با لیبل میخواهید (مثلاً در صفحهٔ تنظیمات بهجای هدر)، دستی از
useTheme()(صادرشدهٔ next-themes) باSwitchبسازید.