پرتوپرتو

تم‌بندی

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

سیستم تم‌بندی پرتو به شما امکان می‌دهد به راحتی بین تم‌های مختلف جابجا شوید و حتی تم‌های سفارشی خود را ایجاد کنید. سیستم ما بر اساس CSS Variables و Tailwind CSS ساخته شده است که انعطاف‌پذیری کامل را فراهم می‌کند.

تغییر تم

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

تم‌های پیش‌فرض

دیزاین سیستم پرتو با 3 تم پیش‌فرض عرضه می‌شود:

تم روشن (Light)

تم پیش‌فرض برای محیط‌های روشن با پالت رنگی سفید و خاکستری روشن.

/* theme-light.css */
.light {
  --background-default: 0deg 0% 100%;
  --foreground-default: 222.2deg 47.4% 11.2%;
  --foreground-light: 215.4deg 16.3% 46.9%;
  --foreground-lighter: 216deg 12.2% 83.9%;
  --foreground-muted: 220deg 8.9% 46.1%;
  /* ... */
}

بهترین برای:

  • وب‌سایت‌های عمومی
  • اپلیکیشن‌های اداری
  • محیط‌های با نور زیاد

تم تاریک (Dark)

تم مدرن برای محیط‌های کم‌نور با پالت رنگی تیره و خاکستری تاریک.

/* theme-dark.css */
.dark {
  --background-default: 222.2deg 84% 4.9%;
  --foreground-default: 210deg 40% 98%;
  --foreground-light: 217.9deg 10.6% 64.9%;
  --foreground-lighter: 215deg 20.2% 65.1%;
  --foreground-muted: 215.4deg 16.3% 46.9%;
  /* ... */
}

بهترین برای:

  • کار طولانی‌مدت
  • محیط‌های کم‌نور
  • کاربران حساس به نور

تم کلاسیک تاریک (Classic Dark)

نسخه کلاسیک‌تر تم تاریک با کنتراست بیشتر.

/* theme-classic-dark.css */
.classic-dark {
  --background-default: 0deg 0% 8.6%;
  --foreground-default: 0deg 0% 92.9%;
  /* ... */
}

بهترین برای:

  • کاربران علاقه‌مند به ظاهر کلاسیک
  • محیط‌های حرفه‌ای

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

استفاده پایه

برای فعال‌سازی تم، کلاس مربوطه را به تگ html یا body اضافه کنید:

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

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

برای تغییر دینامیک تم، می‌توانید از یک hook ساده استفاده کنید:

// hooks/use-theme.ts
'use client'

import { useEffect, useState } from 'react'

type Theme = 'light' | 'dark' | 'classic-dark'

export function useTheme() {
  const [theme, setTheme] = useState<Theme>('light')

  useEffect(() => {
    // بارگذاری تم از localStorage
    const savedTheme = localStorage.getItem('theme') as Theme
    if (savedTheme) {
      setTheme(savedTheme)
      document.documentElement.className = savedTheme
    }
  }, [])

  const changeTheme = (newTheme: Theme) => {
    setTheme(newTheme)
    localStorage.setItem('theme', newTheme)
    document.documentElement.className = newTheme
  }

  return { theme, changeTheme }
}

کامپوننت سوئیچ تم

// components/theme-switcher.tsx
'use client'

import { useTheme } from '@/hooks/use-theme'
import { Button } from '@parto/ui'

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

  return (
    <div className="flex gap-2">
      <Button
        variant={theme === 'light' ? 'default' : 'outline'}
        onClick={() => changeTheme('light')}
      >
        روشن
      </Button>
      <Button
        variant={theme === 'dark' ? 'default' : 'outline'}
        onClick={() => changeTheme('dark')}
      >
        تاریک
      </Button>
      <Button
        variant={theme === 'classic-dark' ? 'default' : 'outline'}
        onClick={() => changeTheme('classic-dark')}
      >
        کلاسیک
      </Button>
    </div>
  )
}

تشخیص تم سیستم

برای تشخیص خودکار تم بر اساس تنظیمات سیستم:

'use client'

import { useEffect, useState } from 'react'

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

  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
}

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

ایجاد فایل تم

برای ایجاد تم سفارشی، یک فایل CSS جدید بسازید:

/* theme-custom.css */
.custom-theme {
  /* رنگ‌های پس‌زمینه */
  --background-default: 280deg 40% 10%;
  --background-200: 280deg 40% 12%;
  --background-alternative-default: 260deg 40% 15%;
  --background-alternative-200: 260deg 40% 18%;
  
  /* سطوح */
  --background-surface-75: 280deg 35% 15%;
  --background-surface-100: 280deg 35% 18%;
  --background-surface-200: 280deg 35% 22%;
  --background-surface-300: 280deg 35% 26%;
  --background-surface-400: 280deg 35% 30%;
  
  /* رنگ‌های متن */
  --foreground-default: 280deg 20% 95%;
  --foreground-light: 280deg 15% 75%;
  --foreground-lighter: 280deg 10% 60%;
  --foreground-muted: 280deg 8% 50%;
  --foreground-contrast: 0deg 0% 100%;
  
  /* حاشیه‌ها */
  --border-default: 280deg 30% 25%;
  --border-strong: 280deg 30% 35%;
  --border-stronger: 280deg 30% 45%;
  --border-overlay: 280deg 25% 30%;
  --border-control: 280deg 25% 28%;
  
  /* رنگ برند */
  --brand-default: 280deg 80% 60%;
  --brand-200: 280deg 80% 70%;
  --brand-300: 280deg 80% 65%;
  --brand-400: 280deg 80% 55%;
  --brand-500: 280deg 80% 50%;
  --brand-600: 280deg 80% 45%;
  
  /* هشدار */
  --warning-default: 45deg 95% 60%;
  --warning-200: 45deg 95% 75%;
  --warning-300: 45deg 95% 70%;
  --warning-400: 45deg 95% 55%;
  --warning-500: 45deg 95% 50%;
  --warning-600: 45deg 95% 45%;
  
  /* خطر */
  --destructive-default: 0deg 85% 60%;
  --destructive-200: 0deg 85% 75%;
  --destructive-300: 0deg 85% 70%;
  --destructive-400: 0deg 85% 55%;
  --destructive-500: 0deg 85% 50%;
  --destructive-600: 0deg 85% 45%;
}

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

فایل تم را در globals.css وارد کنید:

/* app/globals.css */
@import "./supabase/global.css";
@import "./supabase/theme-light.css";
@import "./supabase/theme-dark.css";
@import "./supabase/theme-classic-dark.css";
@import "./theme-custom.css"; /* تم سفارشی شما */

استفاده از تم سفارشی

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

متغیرهای رنگی اصلی

تمام تم‌ها باید این متغیرهای اصلی را تعریف کنند:

پس‌زمینه‌ها

--background-default
--background-200
--background-alternative-default
--background-alternative-200
--background-selection
--background-control
--background-surface-75
--background-surface-100
--background-surface-200
--background-surface-300
--background-surface-400
--background-overlay-default
--background-overlay-hover
--background-button-default
--background-dialog-default
--background-dash-sidebar
--background-dash-canvas

متن (Foreground)

--foreground-default
--foreground-light
--foreground-lighter
--foreground-muted
--foreground-contrast

حاشیه‌ها

--border-default
--border-strong
--border-stronger
--border-overlay
--border-control
--border-alternative
--border-secondary
--border-button-default
--border-button-hover

رنگ‌های سمانتیک

--brand-default
--brand-200
--brand-300
--brand-400
--brand-500
--brand-600

--warning-default
--warning-200
--warning-300
--warning-400
--warning-500
--warning-600

--destructive-default
--destructive-200
--destructive-300
--destructive-400
--destructive-500
--destructive-600

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

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

همیشه اطمینان حاصل کنید که رنگ‌های متن و پس‌زمینه کنتراست کافی دارند (WCAG AA حداقل 4.5:1):

// استفاده از ابزار محاسبه کنتراست
function calculateContrast(color1: string, color2: string): number {
  // محاسبه contrast ratio
  // باید حداقل 4.5:1 باشد
}

سازگاری با Brand Colors

رنگ‌های برند باید در تمام تم‌ها قابل تشخیص باشند:

/* تم روشن */
.light {
  --brand-default: 153deg 60% 53%;
}

/* تم تاریک */
.dark {
  --brand-default: 155deg 78% 40%;
}

تست در شرایط واقعی

قبل از نهایی کردن تم:

  • ✅ تست در دستگاه‌های مختلف
  • ✅ تست با محتوای واقعی
  • ✅ تست accessibility
  • ✅ تست در نورهای مختلف

مستندسازی تم

برای هر تم سفارشی، موارد زیر را مستند کنید:

# تم سفارشی من

**هدف:** برای داشبورد مدیریتی
**پالت اصلی:** بنفش و آبی
**بهترین برای:** کار شبانه

## رنگ‌های اصلی
- Primary: #7C3AED (بنفش)
- Secondary: #3B82F6 (آبی)
- Background: #1A1B2E (تیره)

## نکات استفاده
- بهتر است در محیط‌های کم‌نور استفاده شود
- برای متن‌های طولانی مناسب نیست

مثال کامل: سیستم تم‌بندی

// app/providers.tsx
'use client'

import { createContext, useContext, useEffect, useState } from 'react'

type Theme = 'light' | 'dark' | 'classic-dark' | 'system'
type ResolvedTheme = 'light' | 'dark' | 'classic-dark'

interface ThemeContextType {
  theme: Theme
  resolvedTheme: ResolvedTheme
  setTheme: (theme: Theme) => void
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setThemeState] = useState<Theme>('system')
  const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>('light')

  useEffect(() => {
    // بارگذاری تم از localStorage
    const savedTheme = localStorage.getItem('theme') as Theme
    if (savedTheme) {
      setThemeState(savedTheme)
    }
  }, [])

  useEffect(() => {
    const root = document.documentElement

    if (theme === 'system') {
      // تشخیص تم سیستم
      const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
      const systemTheme = mediaQuery.matches ? 'dark' : 'light'
      setResolvedTheme(systemTheme)
      root.className = systemTheme

      // گوش دادن به تغییرات
      const handler = (e: MediaQueryListEvent) => {
        const newTheme = e.matches ? 'dark' : 'light'
        setResolvedTheme(newTheme)
        root.className = newTheme
      }

      mediaQuery.addEventListener('change', handler)
      return () => mediaQuery.removeEventListener('change', handler)
    } else {
      setResolvedTheme(theme)
      root.className = theme
      localStorage.setItem('theme', theme)
    }
  }, [theme])

  const setTheme = (newTheme: Theme) => {
    setThemeState(newTheme)
  }

  return (
    <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

export function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider')
  }
  return context
}
// app/layout.tsx
import { ThemeProvider } from './providers'

export default function RootLayout({ children }) {
  return (
    <html lang="fa" dir="rtl">
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}
// components/theme-switcher.tsx
'use client'

import { useTheme } from '@/app/providers'
import { Button } from '@parto/ui'

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

  return (
    <div className="flex gap-2">
      <Button
        variant={theme === 'light' ? 'default' : 'outline'}
        size="sm"
        onClick={() => setTheme('light')}
      >
        ☀️ روشن
      </Button>
      <Button
        variant={theme === 'dark' ? 'default' : 'outline'}
        size="sm"
        onClick={() => setTheme('dark')}
      >
        🌙 تاریک
      </Button>
      <Button
        variant={theme === 'classic-dark' ? 'default' : 'outline'}
        size="sm"
        onClick={() => setTheme('classic-dark')}
      >
        🌑 کلاسیک
      </Button>
      <Button
        variant={theme === 'system' ? 'default' : 'outline'}
        size="sm"
        onClick={() => setTheme('system')}
      >
        💻 سیستم
      </Button>
    </div>
  )
}

Troubleshooting

مشکل: تم تغییر نمی‌کند

// بررسی کنید که کلاس روی html تنظیم شده است
useEffect(() => {
  console.log('Current class:', document.documentElement.className)
}, [])

مشکل: تم در refresh پاک می‌شود

// اطمینان حاصل کنید که localStorage را چک می‌کنید
useEffect(() => {
  const savedTheme = localStorage.getItem('theme')
  if (savedTheme) {
    setTheme(savedTheme as Theme)
  }
}, [])

مشکل: Flash of wrong theme در بارگذاری

// در head قبل از React اجرا شود
// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="fa" dir="rtl">
      <head>
        <script dangerouslySetInnerHTML={{
          __html: `
            (function() {
              const theme = localStorage.getItem('theme') || 'light';
              document.documentElement.className = theme;
            })();
          `
        }} />
      </head>
      <body>{children}</body>
    </html>
  )
}

منابع مرتبط