پرتوپرتو

تم‌بندی

نحوه سفارشی‌سازی و ایجاد تم‌های سفارشی در دیزاین سیستم پرتو — تیره‌اول با دو نشانگر

سیستم تم‌بندی پرتو بر پایه CSS Variables و Tailwind CSS ساخته شده و انعطاف کامل می‌دهد. پرتو تیره-اول است: تم پایه در :root تیره است و روشن یک opt-in صریح است.

تغییر تم

برای تست تم‌های مختلف، از سوئیچ زیر استفاده کنید:

دو تم آماده

دیزاین سیستم پرتو با ۲ تم production-grade عرضه می‌شود. برخلاف نسخه‌های قدیمی، تیره پیش‌فرض است:

تممکانیزمفعال می‌شود با
تاریک (Dark)تم پایه‌ی :root (انتخابگر: :root, [data-theme='dark'], .dark)پیش‌فرض — بدون هیچ نشانگری هم تیره است
روشن (Light)opt-in صریح ([data-theme='light'], .light)<html className="light" data-theme="light">

بلوک روشن بعد از بلوک تیره می‌آید و color-scheme: light را هم ست می‌کند، پس روی specificity برابر برنده می‌شود.

/* شکل واقعی تعریف تم در پکیج (توکن‌ها رنگ کامل‌اند — v2+).
   نوترال‌ها پس از بازکالیبراسیون پارامتریک OKLCH یک ته‌مایه‌ی برند دارند
   (مثلاً hue حدود ۱۲۰/۱۵۰ درجه)، نه خاکستریِ خالص ۰ درجه. */
:root,
[data-theme='dark'],
.dark {
  --background-default: hsl(120deg 2.6% 7.6%);
  --foreground-default: hsl(150deg 5.9% 93.3%);
  /* ... */
}

[data-theme='light'],
.light {
  color-scheme: light;
  --background-default: hsl(0deg 0% 99.2%);
  --foreground-default: hsl(0deg 0% 1.2%);
  /* ... */
}

اگر هیچ نشانگری نگذارید

یک اپ که هیچ تمی روی &lt;html&gt; ست نکند، حالا تیره رندر می‌شود (پیش‌فرض :root). مصرف‌کننده‌ی تیره‌ی صریح (className="dark" data-theme="dark") دقیقاً مثل قبل رندر می‌شود، چون بلوک .dark همان مقادیر را دوباره اعلام می‌کند.

نحوه پیاده‌سازی تم‌بندی

۱. استفاده پایه (تک‌تم تیره)

اگر اپ شما فقط تیره است، کافی است نشانگرها را روی <html> تثبیت کنید — نیازی به next-themes نیست:

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="fa" dir="rtl" className="dark" data-theme="dark">
      <body>{children}</body>
    </html>
  )
}

۲. تعویض داینامیک تم (دو نشانگر)

پرتو تم را با دو نشانگر می‌خواند که باید هم‌گام بمانند: کلاس (.dark/.light) و attribute (data-theme). با next-themes هر دو را با یک آرایه در attribute ست کنید:

// app/providers.tsx
'use client'

import { ThemeProvider } from 'next-themes'

export function Providers({ children }) {
  return (
    <ThemeProvider
      attribute={['class', 'data-theme']}
      defaultTheme="dark"
      themes={['light', 'dark']}
      enableSystem={false}
    >
      {children}
    </ThemeProvider>
  )
}

هم‌گامی نشانگرها الزامی است

اگر فقط یک نشانگر را ست کنید (مثلاً تنها attribute="class")، utilityهای dark: و مقادیر توکن ممکن است وسط تعویض با هم اختلاف پیدا کنند — دلیلی که سایت مستندات یک کامپوننت ThemeSync دارد. با attribute={['class', 'data-theme']} هر دو با هم آپدیت می‌شوند و به این نیاز ندارید.

سوئیچ تم با هوک useTheme:

'use client'

import { useTheme } from 'next-themes'
import { Button } from '@partodata/ui'

export function ThemeSwitcher() {
  const { theme, setTheme } = useTheme()

  return (
    <div className="flex gap-2">
      <Button variant={theme === 'light' ? 'primary' : 'outline'} size="sm" onClick={() => setTheme('light')}>
        روشن
      </Button>
      <Button variant={theme === 'dark' ? 'primary' : 'outline'} size="sm" onClick={() => setTheme('dark')}>
        تاریک
      </Button>
    </div>
  )
}

۳. تشخیص تم سیستم

برای پیروی خودکار از تنظیم سیستم، enableSystem را در next-themes روشن کنید — نیازی به هوک دستی نیست. اگر تم را خودتان مدیریت می‌کنید:

'use client'

import { useEffect, useState } from 'react'

export function useSystemTheme() {
  const [theme, setTheme] = useState<'light' | 'dark'>('dark')

  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
    setTheme(mediaQuery.matches ? 'dark' : 'light')
    const handler = (e: MediaQueryListEvent) => setTheme(e.matches ? 'dark' : 'light')
    mediaQuery.addEventListener('change', handler)
    return () => mediaQuery.removeEventListener('change', handler)
  }, [])

  return theme
}

ساخت تم سفارشی

توکن‌های پرتو (v2+) رنگ کامل هستند — مقدار را مستقیم بنویسید و هرگز داخل hsl() نپیچید. یک تم سفارشی یعنی بازتعریف همان توکن‌ها زیر یک انتخابگر دلخواه.

مدل سطوح: توپُر در برابر شفافِ تطبیقی

همه‌ی سطوح یک نردبان توپُرِ روبه‌روشن نیستند. فقط تایرهای توپُر رنگ ثابت‌اند: --background-default، --background-surface-100، --background-surface-300، --background-overlay-default (سطح منو/پاپ‌اور) و --background-dialog-default. در مقابل، --background-surface-200، --background-surface-400، --background-muted، --background-control، --background-selection، --background-overlay-hover و کل مقیاس حاشیه لایه‌های شفافِ تطبیقی هستند — color-mix(in oklch, var(--foreground-default) N%, transparent) — که روی هر پس‌زمینه‌ای که پشتشان است ترکیب می‌شوند و همراه آن روشن‌تر می‌شوند. در تم سفارشی فقط تایرهای توپُر را با رنگ ثابت بازتعریف کنید؛ اگر لایه‌های تطبیقی را به رنگ ثابت تبدیل کنید، رفتار «ریختن روی بک‌دراپ» که دیزاین‌سیستم به آن تکیه دارد از دست می‌رود.

۱. تعریف تم

/* theme-custom.css — فقط تایرهای توپُر را با رنگ کامل بازتعریف کنید.
   لایه‌های تطبیقی (surface-200/400، muted، control، selection، حاشیه‌ها)
   خودشان از روی foreground بازساخته می‌شوند — دست‌کاری‌شان لازم نیست. */
[data-theme='ocean'],
.ocean {
  color-scheme: dark;

  /* پس‌زمینه‌ها و سطوحِ توپُر */
  --background-default: hsl(210deg 40% 8%);
  --background-surface-100: hsl(210deg 35% 12%);
  --background-surface-300: hsl(210deg 33% 15%);
  --background-overlay-default: hsl(210deg 33% 15%); /* سطح منو/پاپ‌اور */
  --background-dialog-default: hsl(210deg 34% 13%); /* سطح دیالوگ */

  /* متن — لایه‌های تطبیقی به این تکیه دارند */
  --foreground-default: hsl(210deg 20% 96%);
  --foreground-light: hsl(210deg 15% 76%);

  /* برند */
  --brand-default: hsl(199deg 89% 55%);
}

۲. وارد کردن تم سفارشی

فایل تم را بعد از import اصلی پرتو بیاورید:

/* app/globals.css */
@import 'tailwindcss';
@import '@partodata/ui/tailwind.css';
@import './theme-custom.css'; /* تم سفارشی شما */

۳. استفاده

<html lang="fa" dir="rtl" className="ocean" data-theme="ocean">
  <body>{children}</body>
</html>

نکات مهم در طراحی تم

کنتراست مناسب

همیشه اطمینان حاصل کنید که رنگ متن و پس‌زمینه کنتراست کافی دارند (WCAG AA حداقل 4.5:1). توکن‌های معنایی پرتو (text-foreground، text-light، text-muted-foreground) از پیش این آستانه را رعایت می‌کنند.

سازگاری برند بین تم‌ها

رنگ برند را برای هر تم جداگانه تنظیم کنید تا در هر دو خوانا بماند:

/* تیره (پایه) */
:root,
[data-theme='dark'],
.dark {
  --brand-default: hsl(153.1deg 60.2% 52.7%);
}

/* روشن */
[data-theme='light'],
.light {
  --brand-default: hsl(152.9deg 60% 52.9%);
}

همه‌ی کامپوننت‌هایی که از text-brand، bg-brand، border-brand استفاده می‌کنند خودکار آپدیت می‌شوند.

نمودارها

نمودارها رنگ‌ها را در زمان اجرا via useRootStyles() می‌خوانند و هنگام تغییر تم خودکار به‌روز می‌شوند — بدون پیکربندی اضافه.

Troubleshooting

تم تغییر نمی‌کند

مطمئن شوید ThemeProvider با attribute={['class', 'data-theme']} راه‌اندازی شده و کلاس (.dark/.light) و data-theme هر دو روی <html> ظاهر می‌شوند.

Flash of wrong theme در بارگذاری

اگر تم را داینامیک عوض می‌کنید، suppressHydrationWarning را روی <html> بگذارید تا خطای hydration هنگام تعویض تم قبل از رندر کلاینت رخ ندهد.

تم تیره کلاسیک اعمال نمی‌شود (فقط Tailwind v3)

بررسی کنید darkMode: ['class', '[data-theme="dark"]'] در tailwind.config.ts تنظیم شده باشد. در مسیر Tailwind v4 (یک import)، متغیر dark: از قبل به هر دو نشانگر وصل است.

منابع مرتبط