Темный режим

6. Стилизация

6.1. Tailwind конфигурация

В проекте HRoom используется утилитарный CSS-фреймворк Tailwind CSS, который обеспечивает быструю и гибкую разработку интерфейса без написания собственных CSS-стилей. Tailwind предоставляет множество готовых классов, которые можно комбинировать для создания любого дизайна. Для настройки Tailwind под требования проекта используется файл конфигурации tailwind.config.js.

Основная конфигурация Tailwind

Файл tailwind.config.js определяет кастомные цвета, отступы, шрифты и другие параметры проекта, что обеспечивает единообразие стилей и соответствие дизайн-системе HRoom:

tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        primary: "#7d4cf1",
        "primary-light": "#9369f5",
        secondary: "#10b981",
        warning: "#f59e0b",
        danger: "#ef4444",
        success: "#22c55e",
        info: "#3b82f6",
        
        "gray-50": "#f9fafb",
        "gray-100": "#f3f4f6",
        "gray-200": "#e5e7eb",
        "gray-300": "#d1d5db",
        "gray-400": "#9ca3af",
        "gray-500": "#6b7280",
        "gray-600": "#4b5563",
        "gray-700": "#374151",
        "gray-800": "#1f2937",
        "gray-900": "#111827",
      },
      fontSize: {
        xs: ['0.75rem', { lineHeight: '1rem' }],
        sm: ['0.875rem', { lineHeight: '1.25rem' }],
        base: ['1rem', { lineHeight: '1.5rem' }],
        lg: ['1.125rem', { lineHeight: '1.75rem' }],
        xl: ['1.25rem', { lineHeight: '1.75rem' }],
        '2xl': ['1.5rem', { lineHeight: '2rem' }],
        '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
        '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
        '5xl': ['3rem', { lineHeight: '1' }],
      },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
      },
      spacing: {
        '72': '18rem',
        '80': '20rem',
        '96': '24rem',
      },
      boxShadow: {
        sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
        DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
        md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
        lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
        xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
        '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
        inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
        none: 'none',
      },
      borderRadius: {
        'none': '0',
        'sm': '0.125rem',
        DEFAULT: '0.25rem',
        'md': '0.375rem',
        'lg': '0.5rem',
        'xl': '0.75rem',
        '2xl': '1rem',
        '3xl': '1.5rem',
        'full': '9999px',
      },
      transitionProperty: {
        'height': 'height',
        'max-height': 'max-height',
        'spacing': 'margin, padding',
      },
      opacity: {
        '0': '0',
        '5': '0.05',
        '10': '0.1',
        '20': '0.2',
        '25': '0.25',
        '30': '0.3',
        '40': '0.4',
        '50': '0.5',
        '60': '0.6',
        '70': '0.7',
        '75': '0.75',
        '80': '0.8',
        '90': '0.9',
        '95': '0.95',
        '100': '1',
      },
      zIndex: {
        '0': 0,
        '10': 10,
        '20': 20,
        '30': 30,
        '40': 40,
        '50': 50,
        '60': 60,
        '70': 70,
        '80': 80,
        '90': 90,
        '100': 100,
        'auto': 'auto',
      },
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
  ],
  darkMode: 'class',
}

Использование цветовой палитры

Цветовая палитра HRoom основана на фирменных цветах продукта и включает основной цвет (primary), дополнительные цвета и набор нейтральных серых оттенков. Эти цвета применяются для обеспечения единообразного внешнего вида всех компонентов приложения.

primary

Основной цвет интерфейса, используется для акцентных элементов и кнопок

primary-light

Светлый вариант основного цвета, используется для hover-состояний

secondary

Дополнительный цвет, используется для второстепенных элементов

warning

Цвет предупреждений и уведомлений

danger

Цвет ошибок и критичных действий

success

Цвет успешных действий и подтверждений

Подключение и настройка плагинов

В проекте используется плагин @tailwindcss/forms, который улучшает стилизацию форм по умолчанию, делая элементы ввода более привлекательными без необходимости дополнительных классов.

PostCSS конфигурация

Для обработки Tailwind CSS и других возможностей стилизации в проекте используется PostCSS. Конфигурация находится в файле postcss.config.js:

postcss.config.js
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

Преимущества использования Tailwind в проекте HRoom

  • Быстрая разработка - благодаря готовым утилитарным классам скорость разработки UI значительно увеличивается
  • Консистентность - все компоненты используют одинаковые значения цветов, отступов и других параметров
  • Адаптивность - легкое создание отзывчивых интерфейсов с помощью адаптивных префиксов
  • Меньший размер CSS - благодаря PurgeCSS, который удаляет неиспользуемые стили при сборке
  • Легкая поддержка - стили находятся непосредственно в HTML/JSX коде, что облегчает поддержку

Пример использования Tailwind CSS в компонентах

Пример компонента с использованием классов Tailwind CSS:

Button.tsx
import React from 'react';

interface ButtonProps {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'danger' | 'outline';
  size?: 'sm' | 'md' | 'lg';
  onClick?: () => void;
  disabled?: boolean;
  className?: string;
}

const Button: React.FC<ButtonProps> = ({
  children,
  variant = 'primary',
  size = 'md',
  onClick,
  disabled = false,
  className = '',
}) => {
  const baseClasses = "inline-flex items-center justify-center font-medium rounded-md transition-colors";
  
  const variantClasses = {
    primary: "bg-primary hover:bg-primary-light text-white",
    secondary: "bg-secondary hover:bg-green-600 text-white",
    danger: "bg-danger hover:bg-red-700 text-white",
    outline: "border border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
  };
  
  const sizeClasses = {
    sm: "px-2.5 py-1.5 text-xs",
    md: "px-4 py-2 text-sm",
    lg: "px-6 py-3 text-base"
  };
  
  const disabledClasses = disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer";
  
  const buttonClasses = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabledClasses} ${className}`;
  
  return (
    <button
      className={buttonClasses}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
};

6.2. Кастомные стили

Помимо использования Tailwind CSS, в проекте HRoom также применяются кастомные стили для реализации уникальных компонентов и эффектов, которые сложно создать только с помощью утилитарных классов. Кастомные стили организованы таким образом, чтобы дополнять Tailwind и не создавать конфликтов.

Организация кастомных стилей

Кастомные стили в HRoom организованы с использованием следующих подходов:

  1. Глобальные стили - определены в файле src/index.css и применяются ко всему приложению
  2. Компонентные стили - определены в отдельных файлах стилей рядом с компонентами
  3. Стили модулей - специфичные стили для конкретных модулей приложения
  4. Динамические стили - стили, создаваемые с помощью JavaScript

Глобальные стили

Глобальные стили в файле src/index.css включают базовые настройки, переменные CSS и стили, которые должны применяться ко всему приложению:

src/index.css
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

/* Импорт шрифтов */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');

/* CSS переменные для глобального использования */
:root {
  --primary-color: #7d4cf1;
  --primary-light-color: #9369f5;
  --secondary-color: #10b981;
  --warning-color: #f59e0b;
  --danger-color: #ef4444;
  --success-color: #22c55e;
  
  /* Переменные для темного режима */
  --dark-bg-color: #1a1a22;
  --dark-card-bg: #25232e;
  --dark-header-bg: #31293d;
  
  /* Размеры для адаптивного дизайна */
  --header-height: 64px;
  --sidebar-width: 260px;
  --sidebar-collapsed-width: 64px;
}

/* Базовые стили */
html {
  font-family: 'Inter', sans-serif;
  scrollbar-width: thin;
  scrollbar-color: var(--primary-color) #f1f1f1;
}

body {
  @apply antialiased text-gray-800 bg-gray-50 dark:bg-gray-900 dark:text-gray-200;
  transition: background-color 0.3s ease;
}

/* Кастомный скроллбар */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 4px;
}

::-webkit-scrollbar-thumb {
  background: #d1d5db;
  border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--primary-color);
}

/* Дополнительные утилитарные классы */
@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
  
  .no-scrollbar::-webkit-scrollbar {
    display: none;
  }
  
  .no-scrollbar {
    -ms-overflow-style: none;
    scrollbar-width: none;
  }
}

/* Кастомные компоненты */
@layer components {
  .card {
    @apply bg-white dark:bg-gray-800 rounded-lg shadow-md p-4;
  }
  
  .form-input {
    @apply w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary focus:border-primary;
  }
  
  .btn {
    @apply inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary hover:bg-primary-light focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-colors;
  }
  
  .btn-secondary {
    @apply bg-secondary hover:bg-green-600;
  }
  
  .btn-danger {
    @apply bg-danger hover:bg-red-700;
  }
  
  .badge {
    @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
  }
  
  .badge-success {
    @apply bg-green-100 text-green-800;
  }
  
  .badge-warning {
    @apply bg-yellow-100 text-yellow-800;
  }
  
  .badge-danger {
    @apply bg-red-100 text-red-800;
  }
}

Компонентные стили с использованием CSS-in-JS

Для некоторых компонентов HRoom использует подход CSS-in-JS, где стили определяются непосредственно в файлах компонентов, что делает их более изолированными. Пример такого подхода:

src/pages/Dashboard/InsightsSlider/styles.ts
export const sliderStyles = {
  container: `
    relative 
    w-full 
    overflow-hidden 
    bg-white 
    dark:bg-gray-800 
    rounded-lg 
    shadow-md 
    p-4
  `,
  slider: `
    flex 
    transition-transform 
    ease-out 
    duration-500
  `,
  card: `
    flex-shrink-0 
    w-full 
    md:w-1/2 
    lg:w-1/3 
    xl:w-1/4 
    px-2
  `,
  navigationButton: `
    absolute 
    top-1/2 
    -translate-y-1/2 
    bg-white 
    dark:bg-gray-700 
    rounded-full 
    p-2 
    shadow-md 
    hover:bg-gray-100 
    dark:hover:bg-gray-600 
    z-10
  `,
  prevButton: `
    left-2
  `,
  nextButton: `
    right-2
  `,
  paginationContainer: `
    flex 
    justify-center 
    mt-4 
    space-x-2
  `,
  paginationDot: `
    w-2 
    h-2 
    rounded-full 
    bg-gray-300 
    dark:bg-gray-600
  `,
  activeDot: `
    bg-primary
  `,
};

Модульные стили

Для более сложных модулей используются отдельные файлы стилей, которые импортируются только в те компоненты, где они нужны. Это помогает поддерживать стили модулей изолированными друг от друга:

src/pages/Dashboard/EngagementGauge/constants.ts
export const COLORS = {
  LOW: '#ef4444', // Red for low scores
  MEDIUM: '#f59e0b', // Amber for medium scores
  HIGH: '#10b981', // Green for high scores
  BACKGROUND: '#f3f4f6', // Light gray background
  TICK: '#9ca3af', // Gray for gauge ticks
};

export const RADIAN = Math.PI / 180;

// Segments for the gauge (0 to 5 rating scale)
export const SEGMENTS = [
  { value: 1, label: '1', color: COLORS.LOW },
  { value: 2, label: '2', color: COLORS.LOW },
  { value: 3, label: '3', color: COLORS.MEDIUM },
  { value: 4, label: '4', color: COLORS.MEDIUM },
  { value: 5, label: '5', color: COLORS.HIGH },
];

// Definitions for tooltip text based on score ranges
export const SCORE_DEFINITIONS = {
  LOW: 'Низкий уровень вовлеченности. Рекомендуется срочно принять меры.',
  MEDIUM: 'Средний уровень вовлеченности. Есть возможности для улучшения.',
  HIGH: 'Высокий уровень вовлеченности. Продолжайте поддерживать этот уровень.',
};

Динамические стили с использованием JavaScript

Для создания динамических стилей, зависящих от состояния компонента или данных, используется подход с вычисляемыми стилями:

Пример использования динамических стилей
import React from 'react';

// Функция для определения цвета на основе значения
const getColorForValue = (value: number) => {
  if (value >= 4.5) return '#10b981'; // Green for excellent
  if (value >= 3.5) return '#34d399'; // Light green for good
  if (value >= 2.5) return '#f59e0b'; // Amber for average
  if (value >= 1.5) return '#fbbf24'; // Yellow for below average
  return '#ef4444'; // Red for poor
};

interface HeatmapCellProps {
  value: number;
}

const HeatmapCell: React.FC<HeatmapCellProps> = ({ value }) => {
  // Динамически вычисляемые стили на основе значения
  const cellStyle = {
    backgroundColor: getColorForValue(value),
    color: value > 3 ? '#ffffff' : '#000000',
    width: '56px',
    height: '56px',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: '6px',
    fontWeight: 500,
    fontSize: '18px',
    transition: 'transform 0.2s ease',
    cursor: 'pointer',
  };
  
  return (
    <div 
      style={cellStyle}
      className="hover:scale-105"
    >
      {value.toFixed(1)}
    </div>
  );
};

Кастомные анимации

В проекте также используются кастомные анимации для создания более интерактивного пользовательского опыта:

Пример кастомной анимации
/* Пример кастомных анимаций в CSS */
@keyframes pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

@keyframes bounce {
  0%, 100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-10px);
  }
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

/* Применение анимаций к элементам */
.animate-pulse {
  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

.animate-bounce {
  animation: bounce 1s infinite;
}

.animate-spin {
  animation: spin 1s linear infinite;
}

Использование кастомных стилей совместно с Tailwind

Таким образом, в проекте HRoom применяется гибридный подход к стилизации:

  • Tailwind CSS используется для большинства повторяющихся стилей и компонентов
  • Кастомные стили применяются для сложных, уникальных компонентов и специфических случаев
  • CSS-переменные используются для единообразия цветов и размеров
  • CSS-in-JS подход для компонентов, требующих динамических стилей

Такой комбинированный подход позволяет получить преимущества быстрой разработки с Tailwind CSS и при этом сохранить гибкость кастомных стилей для специфических требований дизайна.

6.3. Адаптивный дизайн

Проект HRoom разработан с учетом принципов адаптивного дизайна, что обеспечивает корректное отображение и удобство использования приложения на устройствах с различными размерами экранов — от мобильных телефонов до настольных компьютеров. Адаптивность реализована с использованием возможностей Tailwind CSS и дополнительных кастомных решений.

Подход к адаптивному дизайну

В HRoom используется подход "Mobile First", при котором базовый дизайн оптимизирован для мобильных устройств, а затем расширяется для более крупных экранов. Это обеспечивает лучшую производительность и пользовательский опыт на мобильных устройствах.

Адаптивные брейкпоинты

В проекте используются следующие стандартные брейкпоинты Tailwind CSS для создания адаптивного интерфейса:

Брейкпоинт Префикс Ширина экрана Устройства
По умолчанию - Все размеры Мобильные устройства
Small sm: 640px и выше Большие смартфоны, малые планшеты
Medium md: 768px и выше Планшеты в портретной ориентации
Large lg: 1024px и выше Планшеты в альбомной ориентации, маленькие ноутбуки
Extra Large xl: 1280px и выше Ноутбуки и настольные компьютеры
2 Extra Large 2xl: 1536px и выше Большие настольные экраны

Использование адаптивных утилит Tailwind

Tailwind CSS предоставляет простой способ применения адаптивных стилей с помощью префиксов брейкпоинтов. Пример использования адаптивных классов в компонентах HRoom:

Пример адаптивного компонента
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
  {/* Карточки, которые меняют своё расположение в зависимости от размера экрана */}
  <div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
    <h3 className="text-lg sm:text-xl font-medium">Заголовок карточки</h3>
    <p className="mt-2 text-sm sm:text-base">
      Содержимое карточки, которое адаптируется к размеру экрана.
    </p>
  </div>
  {/* Другие карточки... */}
</div>

Адаптивный макет приложения

Общий макет приложения HRoom также адаптивен. На мобильных устройствах боковая панель скрывается, а контент занимает всю доступную ширину экрана. Макет определен в компоненте Layout.tsx:

src/components/Layout.tsx
import React, { useState } from 'react';
import Sidebar from './Sidebar';

interface LayoutProps {
  children: React.ReactNode;
}

const Layout: React.FC<LayoutProps> = ({ children }) => {
  const [sidebarOpen, setSidebarOpen] = useState(false);
  
  return (
    <div className="flex h-screen bg-gray-100 dark:bg-gray-900">
      {/* Мобильная версия сайдбара (перекрывает содержимое) */}
      <div 
        className={`
          fixed inset-0 bg-gray-600 bg-opacity-75 z-20 
          transition-opacity duration-300 ease-linear
          lg:hidden
          ${sidebarOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}
        `}
        onClick={() => setSidebarOpen(false)}
      />
      
      {/* Сайдбар */}
      <div 
        className={`
          fixed z-30 inset-y-0 left-0 w-64 transition duration-300 ease-in-out transform 
          bg-gray-800 dark:bg-gray-900
          lg:translate-x-0 lg:static lg:inset-0
          ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
        `}
      >
        <Sidebar onClose={() => setSidebarOpen(false)} />
      </div>
      
      {/* Основное содержимое */}
      <div className="flex flex-col flex-1 overflow-hidden">
        <header className="bg-white dark:bg-gray-800 shadow-sm z-10">
          <div className="px-4 py-3 flex items-center justify-between">
            {/* Кнопка открытия мобильного меню */}
            <button
              className="text-gray-500 hover:text-gray-600 lg:hidden focus:outline-none"
              onClick={() => setSidebarOpen(true)}
            >
              <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
              </svg>
            </button>
            
            {/* Заголовок страницы на мобильных устройствах */}
            <h1 className="text-lg font-medium lg:hidden">HRoom</h1>
            
            {/* Остальные элементы хедера */}
            <div className="flex items-center space-x-4">
              {/* Профиль, уведомления и т.д. */}
            </div>
          </div>
        </header>
        
        <main className="flex-1 overflow-y-auto p-4 md:p-6">
          {children}
        </main>
      </div>
    </div>
  );
};

Адаптивные компоненты

В HRoom реализованы различные адаптивные компоненты, которые изменяют свою структуру и отображение в зависимости от размера экрана:

Адаптивный дашборд

Компоненты дашборда изменяют свое расположение на разных устройствах, переходя от многоколоночного макета на больших экранах к одноколоночному на мобильных:

Адаптивный макет дашборда
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
  {/* Компонент датчика вовлеченности */}
  <div className="md:col-span-2 xl:col-span-1">
    <EngagementGauge />
  </div>
  
  {/* Компонент инсайтов */}
  <div className="xl:col-span-2">
    <InsightsSlider />
  </div>
  
  {/* Компонент тепловой карты отделов */}
  <div className="md:col-span-2 xl:col-span-3">
    <DepartmentsHeatmap />
  </div>
</div>

Адаптивные таблицы

Таблицы в HRoom адаптируются к разным размерам экрана, используя горизонтальную прокрутку на мобильных устройствах и иной способ отображения данных:

Адаптивные таблицы
// Для экранов меньше md (768px) таблица получает горизонтальную прокрутку
<div className="overflow-x-auto">
  <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
    {/* Содержимое таблицы */}
  </table>
</div>

// Альтернативный подход для очень маленьких экранов - карточный вид вместо таблицы
<div className="block md:hidden">
  {data.map((item) => (
    <div key={item.id} className="bg-white dark:bg-gray-800 shadow rounded-lg p-4 mb-4">
      <div className="flex justify-between items-center">
        <h3 className="text-lg font-medium">{item.name}</h3>
        <span className="text-sm text-gray-500">{item.date}</span>
      </div>
      <div className="mt-2">
        <p><span className="font-medium">Email:</span> {item.email}</p>
        <p><span className="font-medium">Статус:</span> {item.status}</p>
      </div>
    </div>
  ))}
</div>

<div className="hidden md:block">
  <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
    {/* Содержимое таблицы для больших экранов */}
  </table>
</div>

Отзывчивые графики и визуализации

Графики и визуализации в HRoom используют библиотеку Recharts, которая обеспечивает адаптивность. Графики автоматически подстраиваются под доступную ширину контейнера:

Адаптивные графики
import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';

// Компонент с адаптивным графиком
const MetricsChart = ({ data }) => {
  return (
    <div className="h-64 md:h-80">
      <ResponsiveContainer width="100%" height="100%">
        <LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
          <CartesianGrid strokeDasharray="3 3" stroke="#e0e0e0" />
          <XAxis dataKey="date" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Line type="monotone" dataKey="engagement" stroke="#7d4cf1" activeDot={{ r: 8 }} />
          <Line type="monotone" dataKey="communication" stroke="#10b981" />
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
};

Использование адаптивных изображений и SVG

В проекте используются адаптивные изображения и SVG-графика, которые корректно масштабируются на разных устройствах:

Адаптивные изображения и SVG
// Адаптивное изображение
<img 
  src="/images/logo.png" 
  alt="HRoom Logo" 
  className="w-full max-w-md mx-auto h-auto" 
  srcSet="/images/logo-small.png 400w, /images/logo-medium.png 800w, /images/logo.png 1200w"
  sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
/>

// SVG иконка, которая масштабируется
<svg 
  viewBox="0 0 24 24" 
  className="w-6 h-6 md:w-8 md:h-8" 
  fill="currentColor"
>
  <path d="M12 2a4 4 0 014 4v2h2a2 2 0 012 2v10a2 2 0 01-2 2H6a2 2 0 01-2-2V10a2 2 0 012-2h2V6a4 4 0 014-4zm0 18a1 1 0 100-2 1 1 0 000 2zm0-14a2 2 0 00-2 2v2h4V8a2 2 0 00-2-2z" />
</svg>

Медиа-запросы для кастомных адаптивных стилей

В дополнение к возможностям Tailwind, в проекте используются и кастомные медиа-запросы для более тонкой настройки адаптивного поведения:

Кастомные медиа-запросы
/* Кастомные стили для разных размеров экрана */
@media (max-width: 640px) {
  .dashboard-card {
    padding: 1rem;
    margin-bottom: 1rem;
  }
  
  .chart-container {
    height: 200px;
  }
}

@media (min-width: 641px) and (max-width: 1024px) {
  .dashboard-card {
    padding: 1.5rem;
  }
  
  .chart-container {
    height: 300px;
  }
}

@media (min-width: 1025px) {
  .dashboard-card {
    padding: 2rem;
  }
  
  .chart-container {
    height: 400px;
  }
}

/* Пример использования медиа-запросов для ориентации устройства */
@media (orientation: portrait) {
  .landscape-only {
    display: none;
  }
  
  .portrait-notice {
    display: block;
  }
}

@media (orientation: landscape) {
  .portrait-only {
    display: none;
  }
  
  .landscape-notice {
    display: block;
  }
}

Тестирование адаптивного дизайна

Для обеспечения корректной работы адаптивного дизайна, в проекте HRoom применяются следующие методы тестирования:

  • Эмуляция устройств в браузере - использование инструментов разработчика в браузерах для эмуляции различных устройств
  • Реальное тестирование на устройствах - проверка интерфейса на физических мобильных устройствах и планшетах
  • Автоматизированное тестирование - использование инструментов для проверки адаптивности, например, инспекция Viewport в Chrome DevTools
  • Визуальное тестирование - сравнение скриншотов с эталонными для разных размеров экрана

Выводы

Адаптивный дизайн в HRoom обеспечивает следующие преимущества:

  • Единая кодовая база для всех устройств, что упрощает разработку и поддержку
  • Улучшенный пользовательский опыт на всех типах устройств
  • Поддержка будущих устройств благодаря гибкому дизайну
  • Лучшая доступность для пользователей с различными потребностями
  • Улучшенная производительность благодаря оптимизации контента под размер экрана

6.4. Темизация

HRoom предоставляет пользователям возможность выбора между светлой и темной темами оформления для большего комфорта использования в разных условиях освещения и соответствия личным предпочтениям. Система темизации реализована с использованием CSS-переменных, Tailwind CSS и JavaScript.

Архитектура системы темизации

Система темизации в HRoom построена на следующих принципах:

  1. CSS-переменные для определения цветов и других параметров темы
  2. Атрибут data-theme на корневом элементе для переключения тем
  3. Встроенная поддержка тёмного режима в Tailwind CSS через директиву darkMode: 'class'
  4. Сохранение выбора пользователя в localStorage для персистентности между сессиями
  5. Автоматическое определение предпочтений пользователя с помощью медиа-запроса prefers-color-scheme

CSS-переменные для темизации

Основные цвета и параметры тем определены в CSS-переменных, что позволяет легко переключаться между темами:

CSS-переменные для тем
:root {
  /* Переменные светлой темы */
  --primary-color: #7d4cf1;
  --primary-light-color: #9369f5;
  --secondary-color: #10b981;
  --warning-color: #f59e0b;
  --danger-color: #ef4444;
  --success-color: #22c55e;
  
  /* Цвета фона и текста */
  --bg-color: #f9fafb;
  --card-bg-color: #ffffff;
  --header-bg-color: #ffffff;
  --text-color: #111827;
  --text-secondary-color: #4b5563;
  --border-color: #e5e7eb;
  
  /* Параметры тени */
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}

/* Переменные темной темы */
[data-theme="dark"] {
  --primary-color: #9369f5;
  --primary-light-color: #a47ff6;
  --secondary-color: #34d399;
  --warning-color: #fbbf24;
  --danger-color: #f87171;
  --success-color: #4ade80;
  
  /* Цвета фона и текста */
  --bg-color: #1a1a22;
  --card-bg-color: #25232e;
  --header-bg-color: #31293d;
  --text-color: #f9fafb;
  --text-secondary-color: #9ca3af;
  --border-color: #374151;
  
  /* Параметры тени */
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.25);
  --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px 0 rgba(0, 0, 0, 0.2);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.15);
}

Настройка Tailwind для темизации

Для поддержки темного режима в Tailwind CSS используется директива darkMode: 'class' в файле конфигурации, что позволяет применять стили темного режима через префикс dark::

Настройка Tailwind для темизации
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      // ... остальные настройки темы
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
  ],
  darkMode: 'class', // Включение режима темы через класс
}

JavaScript для управления темами

Для переключения между темами и сохранения выбора пользователя используется JavaScript:

Управление темами с помощью JavaScript
// Функция для установки темы
const setTheme = (theme) => {
  // Устанавливаем атрибут data-theme на корневом элементе
  document.documentElement.setAttribute('data-theme', theme);
  
  // Добавляем/удаляем класс 'dark' для поддержки Tailwind
  if (theme === 'dark') {
    document.documentElement.classList.add('dark');
  } else {
    document.documentElement.classList.remove('dark');
  }
  
  // Сохраняем выбор пользователя в localStorage
  localStorage.setItem('theme', theme);
};

// Функция для переключения темы
const toggleTheme = () => {
  const currentTheme = localStorage.getItem('theme') || 'light';
  const newTheme = currentTheme === 'light' ? 'dark' : 'light';
  setTheme(newTheme);
};

// Функция для инициализации темы при загрузке страницы
const initTheme = () => {
  // Проверяем, есть ли сохраненное значение в localStorage
  const savedTheme = localStorage.getItem('theme');
  
  if (savedTheme) {
    // Если есть, используем его
    setTheme(savedTheme);
  } else {
    // Если нет, проверяем предпочтения системы
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    setTheme(prefersDark ? 'dark' : 'light');
  }
};

// Инициализация темы при загрузке страницы
document.addEventListener('DOMContentLoaded', initTheme);

// Обработчик для переключателя темы
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
  themeToggle.addEventListener('change', () => {
    toggleTheme();
  });
  
  // Устанавливаем начальное состояние переключателя
  themeToggle.checked = localStorage.getItem('theme') === 'dark';
}

Компонент переключателя темы

В HRoom реализован компонент переключателя темы, который позволяет пользователям легко менять тему оформления:

Компонент переключателя темы
import React, { useEffect, useState } from 'react';

const ThemeToggle = () => {
  const [isDarkMode, setIsDarkMode] = useState(false);

  // Инициализация темы при загрузке компонента
  useEffect(() => {
    // Проверяем начальное предпочтение
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    setDarkMode(mediaQuery.matches);

    // Создаем слушатель изменений
    const handleChange = (e) => {
      setDarkMode(e.matches);
    };

    // Подписываемся на изменения
    if (mediaQuery.addEventListener) {
      mediaQuery.addEventListener('change', handleChange);
    } else {
      // Для старых браузеров
      mediaQuery.addListener(handleChange);
    }

    // Отписываемся при размонтировании
    return () => {
      if (mediaQuery.removeEventListener) {
        mediaQuery.removeEventListener('change', handleChange);
      } else {
        // Для старых браузеров
        mediaQuery.removeListener(handleChange);
      }
    };
  }, []);

  // Обработчик переключения темы
  const toggleTheme = () => {
    const newTheme = isDarkMode ? 'light' : 'dark';
    setIsDarkMode(!isDarkMode);
    
    document.documentElement.setAttribute('data-theme', newTheme);
    
    if (newTheme === 'dark') {
      document.documentElement.classList.add('dark');
    } else {
      document.documentElement.classList.remove('dark');
    }
    
    localStorage.setItem('theme', newTheme);
  };

  return (
    <div className="flex items-center space-x-2">
      <span className="text-sm text-gray-500 dark:text-gray-400">
        {isDarkMode ? 'Темная тема' : 'Светлая тема'}
      </span>
      
      <button
        onClick={toggleTheme}
        className={`
          w-12 h-6 rounded-full p-1 transition-colors duration-300 focus:outline-none
          ${isDarkMode ? 'bg-primary' : 'bg-gray-300'}
        `}
        aria-label={isDarkMode ? 'Переключиться на светлую тему' : 'Переключиться на темную тему'}
      >
        <div
          className={`
            bg-white w-4 h-4 rounded-full shadow-md transform transition-transform duration-300
            ${isDarkMode ? 'translate-x-6' : ''}
          `}
        ></div>
      </button>
      
      {/* Иконки для визуализации выбора темы */}
      <span className={`text-gray-500 dark:text-gray-400 ${isDarkMode ? 'hidden' : 'block'}`}>
        <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
          <path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm0-14a2 2 0 00-2 2v2h4V8a2 2 0 00-2-2z" />
        </svg>
      </span>
      
      <span className={`text-gray-500 dark:text-gray-400 ${isDarkMode ? 'block' : 'hidden'}`}>
        <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
          <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
        </svg>
      </span>
    </div>
  );
};

export default ThemeToggle;

Использование темизации в компонентах

Благодаря поддержке темного режима в Tailwind CSS, компоненты HRoom могут легко адаптироваться к различным темам с помощью префикса dark::

Использование темизации в компонентах
// Пример компонента карточки с поддержкой тем
const Card = ({ children, title }) => {
  return (
    <div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 transition-colors duration-300">
      {title && (
        <h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
          {title}
        </h3>
      )}
      <div className="text-gray-700 dark:text-gray-300">
        {children}
      </div>
    </div>
  );
};

// Пример компонента кнопки с поддержкой тем
const Button = ({ children, onClick, variant = 'primary' }) => {
  const baseClasses = "px-4 py-2 rounded-md font-medium focus:outline-none transition-colors duration-200";
  
  const variantClasses = {
    primary: "bg-primary hover:bg-primary-light text-white dark:bg-primary-light dark:hover:bg-primary",
    secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-white",
    outline: "border border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
  };
  
  return (
    <button
      className={`${baseClasses} ${variantClasses[variant]}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

Работа с изображениями и иконками в разных темах

В HRoom также предусмотрена возможность использования разных изображений и иконок для светлой и темной тем:

Изображения и иконки в разных темах
// Использование разных изображений для разных тем
const ThemeAwareImage = ({ lightSrc, darkSrc, alt, ...props }) => {
  return (
    <>
      <img 
        src={lightSrc} 
        alt={alt} 
        className="block dark:hidden" 
        {...props} 
      />
      <img 
        src={darkSrc} 
        alt={alt} 
        className="hidden dark:block" 
        {...props} 
      />
    </>
  );
};

// Использование SVG с разными цветами для разных тем
const ThemeAwareIcon = ({ className }) => {
  return (
    <svg 
      className={`w-6 h-6 text-gray-800 dark:text-white ${className}`}
      fill="currentColor" 
      viewBox="0 0 20 20"
    >
      <path d="M12 2a4 4 0 014 4v2h2a2 2 0 012 2v10a2 2 0 01-2 2H6a2 2 0 01-2-2V10a2 2 0 012-2h2V6a4 4 0 014-4zm0 18a1 1 0 100-2 1 1 0 000 2zm0-14a2 2 0 00-2 2v2h4V8a2 2 0 00-2-2z" />
    </svg>
  );
};

Отслеживание системных предпочтений

HRoom также может автоматически реагировать на изменение системных настроек темы пользователя:

Отслеживание системных предпочтений
import { useEffect, useState } from 'react';

// Хук для отслеживания системных предпочтений темы
const useSystemThemePreference = () => {
  const [prefersDarkMode, setPrefersDarkMode] = useState(false);

  useEffect(() => {
    // Проверяем начальное предпочтение
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    setPrefersDarkMode(mediaQuery.matches);

    // Создаем слушатель изменений
    const handleChange = (e) => {
      setPrefersDarkMode(e.matches);
    };

    // Подписываемся на изменения
    if (mediaQuery.addEventListener) {
      mediaQuery.addEventListener('change', handleChange);
    } else {
      // Для старых браузеров
      mediaQuery.addListener(handleChange);
    }

    // Отписываемся при размонтировании
    return () => {
      if (mediaQuery.removeEventListener) {
        mediaQuery.removeEventListener('change', handleChange);
      } else {
        // Для старых браузеров
        mediaQuery.removeListener(handleChange);
      }
    };
  }, []);

  return prefersDarkMode;
};

// Использование хука
const ThemeManager = () => {
  const prefersDarkMode = useSystemThemePreference();
  const [theme, setTheme] = useState('auto'); // 'auto', 'light' или 'dark'
  
  useEffect(() => {
    // Если тема установлена на авто, следуем системным предпочтениям
    if (theme === 'auto') {
      if (prefersDarkMode) {
        document.documentElement.setAttribute('data-theme', 'dark');
        document.documentElement.classList.add('dark');
      } else {
        document.documentElement.setAttribute('data-theme', 'light');
        document.documentElement.classList.remove('dark');
      }
    }
  }, [prefersDarkMode, theme]);
  
  const handleThemeChange = (newTheme) => {
    setTheme(newTheme);
    localStorage.setItem('theme-preference', newTheme);
    
    if (newTheme === 'auto') {
      // Если выбран режим 'auto', применяем системные предпочтения
      if (prefersDarkMode) {
        document.documentElement.setAttribute('data-theme', 'dark');
        document.documentElement.classList.add('dark');
      } else {
        document.documentElement.setAttribute('data-theme', 'light');
        document.documentElement.classList.remove('dark');
      }
    } else {
      // Иначе применяем выбранную тему
      document.documentElement.setAttribute('data-theme', newTheme);
      
      if (newTheme === 'dark') {
        document.documentElement.classList.add('dark');
      } else {
        document.documentElement.classList.remove('dark');
      }
    }
  };
  
  return (
    <div className="flex items-center space-x-4">
      <span className="text-sm text-gray-600 dark:text-gray-300">Тема:</span>
      <div className="flex space-x-2">
        <button
          className={`px-3 py-1 rounded-md text-sm ${theme === 'light' ? 'bg-primary text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-white'}`}
          onClick={() => handleThemeChange('light')}
        >
          Светлая
        </button>
        <button
          className={`px-3 py-1 rounded-md text-sm ${theme === 'dark' ? 'bg-primary text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-white'}`}
          onClick={() => handleThemeChange('dark')}
        >
          Темная
        </button>
        <button
          className={`px-3 py-1 rounded-md text-sm ${theme === 'auto' ? 'bg-primary text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-white'}`}
          onClick={() => handleThemeChange('auto')}
        >
          Авто
        </button>
      </div>
    </div>
  );
};

Преимущества системы темизации в HRoom

Реализованная система темизации обеспечивает следующие преимущества:

  • Улучшенная эргономика - снижение нагрузки на глаза в разных условиях освещения
  • Персонализация - пользователь может выбрать предпочтительную тему
  • Соответствие системным настройкам - возможность автоматически следовать системным предпочтениям
  • Экономия заряда батареи - темная тема может экономить заряд на устройствах с OLED-экранами
  • Единообразие стилей - система обеспечивает консистентность оформления в обеих темах

Примеры использования темизации в различных компонентах

Вот несколько примеров того, как компоненты HRoom адаптируются к различным темам:

Формы и поля ввода

В светлой теме используются яркие фоны и темный текст, в темной — темные фоны и светлый текст

Графики и диаграммы

Цвета фона, осей и сеток адаптируются к теме, сохраняя читаемость данных

Уведомления и алерты

Сохраняют заметность и информативность при обеих темах благодаря адаптивным контрастам

Таблицы данных

Используют разные цвета строк и границ в зависимости от темы для лучшей читаемости

Заключение

Система темизации в HRoom является важной частью пользовательского опыта, предоставляя пользователям возможность выбрать комфортный для них вариант оформления. Благодаря использованию современных подходов, таких как CSS-переменные и директива darkMode в Tailwind CSS, система обеспечивает консистентность и удобство как для пользователей, так и для разработчиков.