uchill/front_material/MATERIAL_COMPONENTS_GUIDE.md

20 KiB
Raw Permalink Blame History

🎨 Руководство по Material Web Components 3

Только Material компоненты! Никаких собственных компонентов!


📦 Установка и настройка

npm install @material/web

🔧 Настройка TypeScript

Создать файл types/material-web.d.ts:

// Декларации для Material Web Components
declare namespace JSX {
  interface IntrinsicElements {
    // Buttons
    'md-filled-button': any;
    'md-outlined-button': any;
    'md-text-button': any;
    'md-elevated-button': any;
    'md-tonal-button': any;
    'md-filled-tonal-button': any;
    
    // Text Fields
    'md-filled-text-field': any;
    'md-outlined-text-field': any;
    
    // Cards
    'md-filled-card': any;
    'md-elevated-card': any;
    'md-outlined-card': any;
    
    // Lists
    'md-list': any;
    'md-list-item': any;
    
    // Navigation
    'md-navigation-bar': any;
    'md-navigation-tab': any;
    'md-navigation-drawer': any;
    'md-navigation-drawer-modal': any;
    
    // Dialogs & Sheets
    'md-dialog': any;
    
    // Chips
    'md-chip-set': any;
    'md-assist-chip': any;
    'md-filter-chip': any;
    'md-input-chip': any;
    'md-suggestion-chip': any;
    
    // Icons
    'md-icon': any;
    'md-icon-button': any;
    'md-filled-icon-button': any;
    'md-tonal-icon-button': any;
    'md-outlined-icon-button': any;
    
    // Form Controls
    'md-checkbox': any;
    'md-radio': any;
    'md-switch': any;
    'md-slider': any;
    
    // Select
    'md-filled-select': any;
    'md-outlined-select': any;
    'md-select-option': any;
    
    // Menus
    'md-menu': any;
    'md-menu-item': any;
    'md-sub-menu': any;
    
    // Progress
    'md-circular-progress': any;
    'md-linear-progress': any;
    
    // FAB
    'md-fab': any;
    'md-branded-fab': any;
    
    // Badges
    'md-badge': any;
    
    // Divider
    'md-divider': any;
    
    // Tabs
    'md-tabs': any;
    'md-primary-tab': any;
    'md-secondary-tab': any;
  }
}

📚 Импорт компонентов

Создать файл lib/material-components.ts:

// Buttons
import '@material/web/button/filled-button.js';
import '@material/web/button/outlined-button.js';
import '@material/web/button/text-button.js';
import '@material/web/button/elevated-button.js';
import '@material/web/button/tonal-button.js';

// Text Fields
import '@material/web/textfield/filled-text-field.js';
import '@material/web/textfield/outlined-text-field.js';

// Cards
import '@material/web/labs/card/filled-card.js';
import '@material/web/labs/card/elevated-card.js';
import '@material/web/labs/card/outlined-card.js';

// Lists
import '@material/web/list/list.js';
import '@material/web/list/list-item.js';

// Navigation (из labs)
import '@material/web/labs/navigationbar/navigation-bar.js';
import '@material/web/labs/navigationtab/navigation-tab.js';
import '@material/web/labs/navigationdrawer/navigation-drawer.js';

// Dialogs
import '@material/web/dialog/dialog.js';

// Chips
import '@material/web/chips/chip-set.js';
import '@material/web/chips/assist-chip.js';
import '@material/web/chips/filter-chip.js';
import '@material/web/chips/input-chip.js';
import '@material/web/chips/suggestion-chip.js';

// Icons
import '@material/web/icon/icon.js';
import '@material/web/iconbutton/icon-button.js';
import '@material/web/iconbutton/filled-icon-button.js';
import '@material/web/iconbutton/tonal-icon-button.js';
import '@material/web/iconbutton/outlined-icon-button.js';

// Form Controls
import '@material/web/checkbox/checkbox.js';
import '@material/web/radio/radio.js';
import '@material/web/switch/switch.js';
import '@material/web/slider/slider.js';

// Select
import '@material/web/select/filled-select.js';
import '@material/web/select/outlined-select.js';
import '@material/web/select/select-option.js';

// Menus
import '@material/web/menu/menu.js';
import '@material/web/menu/menu-item.js';
import '@material/web/menu/sub-menu.js';

// Progress
import '@material/web/progress/circular-progress.js';
import '@material/web/progress/linear-progress.js';

// FAB
import '@material/web/fab/fab.js';
import '@material/web/fab/branded-fab.js';

// Badges
import '@material/web/labs/badge/badge.js';

// Divider
import '@material/web/divider/divider.js';

// Tabs
import '@material/web/tabs/tabs.js';
import '@material/web/tabs/primary-tab.js';
import '@material/web/tabs/secondary-tab.js';

Импортировать в app/layout.tsx:

import '@/lib/material-components';

🎨 Material Design 3 Grid System

Создать файл styles/material-grid.css:

/* Material Design 3 Layout Grid */

/* Breakpoints:
   - xs: 0-599px (Mobile)
   - sm: 600-839px (Tablet Portrait)
   - md: 840-1239px (Tablet Landscape / Small Desktop)
   - lg: 1240-1439px (Desktop)
   - xl: 1440px+ (Large Desktop)
*/

.md-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  padding: 0 16px;
  width: 100%;
}

/* Tablet Portrait (600-839px) */
@media (min-width: 600px) {
  .md-grid {
    grid-template-columns: repeat(8, 1fr);
    gap: 24px;
    padding: 0 24px;
  }
}

/* Tablet Landscape / Desktop (840-1239px) */
@media (min-width: 840px) {
  .md-grid {
    grid-template-columns: repeat(12, 1fr);
    gap: 24px;
    padding: 0 24px;
  }
}

/* Large Desktop (1240px+) */
@media (min-width: 1240px) {
  .md-grid {
    gap: 24px;
    padding: 0 24px;
    max-width: 1200px;
    margin: 0 auto;
  }
}

/* Column Span Classes */
/* Mobile (4 columns) */
.md-col-1 { grid-column: span 1; }
.md-col-2 { grid-column: span 2; }
.md-col-3 { grid-column: span 3; }
.md-col-4 { grid-column: span 4; }

/* Tablet (8 columns) */
@media (min-width: 600px) {
  .md-col-sm-1 { grid-column: span 1; }
  .md-col-sm-2 { grid-column: span 2; }
  .md-col-sm-4 { grid-column: span 4; }
  .md-col-sm-6 { grid-column: span 6; }
  .md-col-sm-8 { grid-column: span 8; }
}

/* Desktop (12 columns) */
@media (min-width: 840px) {
  .md-col-md-3 { grid-column: span 3; }
  .md-col-md-4 { grid-column: span 4; }
  .md-col-md-6 { grid-column: span 6; }
  .md-col-md-8 { grid-column: span 8; }
  .md-col-md-9 { grid-column: span 9; }
  .md-col-md-12 { grid-column: span 12; }
}

/* Flexbox альтернатива для простых случаев */
.md-flex {
  display: flex;
  gap: 16px;
}

.md-flex-col {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.md-flex-wrap {
  flex-wrap: wrap;
}

Использование:

<div className="md-grid">
  <div className="md-col-4 md-col-sm-4 md-col-md-6">
    <md-elevated-card>Карточка 1</md-elevated-card>
  </div>
  <div className="md-col-4 md-col-sm-4 md-col-md-6">
    <md-elevated-card>Карточка 2</md-elevated-card>
  </div>
</div>

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

1. Форма входа

'use client';

export default function LoginPage() {
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // Логика входа
  };

  return (
    <div className="md-grid" style={{ minHeight: '100vh', alignItems: 'center' }}>
      <div className="md-col-4 md-col-sm-8 md-col-md-6" style={{ margin: '0 auto' }}>
        <md-elevated-card style={{ padding: '32px' }}>
          <h1 className="md-typescale-headline-medium" style={{ marginBottom: '24px' }}>
            Вход в систему
          </h1>
          
          <form onSubmit={handleSubmit}>
            <md-outlined-text-field
              label="Email"
              type="email"
              required
              style={{ width: '100%', marginBottom: '16px' }}
            ></md-outlined-text-field>
            
            <md-outlined-text-field
              label="Пароль"
              type="password"
              required
              style={{ width: '100%', marginBottom: '24px' }}
            ></md-outlined-text-field>
            
            <md-filled-button type="submit" style={{ width: '100%' }}>
              Войти
            </md-filled-button>
          </form>
        </md-elevated-card>
      </div>
    </div>
  );
}

2. Дашборд с карточками

'use client';

export default function DashboardPage() {
  return (
    <div className="md-grid" style={{ padding: '24px 0' }}>
      {/* Статистика */}
      <div className="md-col-4 md-col-sm-4 md-col-md-4">
        <md-elevated-card style={{ padding: '24px' }}>
          <md-icon style={{ fontSize: '48px', color: 'var(--md-sys-color-primary)' }}>
            groups
          </md-icon>
          <h2 className="md-typescale-headline-small">24</h2>
          <p className="md-typescale-body-medium">Студентов</p>
        </md-elevated-card>
      </div>
      
      <div className="md-col-4 md-col-sm-4 md-col-md-4">
        <md-elevated-card style={{ padding: '24px' }}>
          <md-icon style={{ fontSize: '48px', color: 'var(--md-sys-color-primary)' }}>
            calendar_month
          </md-icon>
          <h2 className="md-typescale-headline-small">12</h2>
          <p className="md-typescale-body-medium">Занятий на неделе</p>
        </md-elevated-card>
      </div>
      
      <div className="md-col-4 md-col-sm-4 md-col-md-4">
        <md-elevated-card style={{ padding: '24px' }}>
          <md-icon style={{ fontSize: '48px', color: 'var(--md-sys-color-primary)' }}>
            payments
          </md-icon>
          <h2 className="md-typescale-headline-small">45000</h2>
          <p className="md-typescale-body-medium">Доход за месяц</p>
        </md-elevated-card>
      </div>
    </div>
  );
}

3. Bottom Navigation Bar (iOS-style)

'use client';

import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';

export function BottomNavigationBar({ userRole }: { userRole: 'mentor' | 'client' | 'parent' }) {
  const pathname = usePathname();
  const router = useRouter();
  const navRef = useRef<any>(null);

  useEffect(() => {
    const nav = navRef.current;
    if (!nav) return;

    const handleNavigation = (e: CustomEvent) => {
      const activeIndex = e.detail.activeIndex;
      const tabs = nav.querySelectorAll('md-navigation-tab');
      const activeTab = tabs[activeIndex];
      const href = activeTab?.getAttribute('data-href');
      if (href) {
        router.push(href);
      }
    };

    nav.addEventListener('navigation-tab-interaction', handleNavigation);
    return () => nav.removeEventListener('navigation-tab-interaction', handleNavigation);
  }, [router]);

  // Меню для ментора
  if (userRole === 'mentor') {
    return (
      <md-navigation-bar ref={navRef} className="ios-bottom-bar">
        <md-navigation-tab 
          label="Главная" 
          data-href="/dashboard"
          active={pathname === '/dashboard'}
        >
          <md-icon slot="inactive-icon">home</md-icon>
          <md-icon slot="active-icon">home</md-icon>
        </md-navigation-tab>
        
        <md-navigation-tab 
          label="Студенты" 
          data-href="/students"
          active={pathname === '/students'}
        >
          <md-icon slot="inactive-icon">group</md-icon>
          <md-icon slot="active-icon">group</md-icon>
        </md-navigation-tab>
        
        <md-navigation-tab 
          label="Расписание" 
          data-href="/schedule"
          active={pathname === '/schedule'}
        >
          <md-icon slot="inactive-icon">calendar_month</md-icon>
          <md-icon slot="active-icon">calendar_month</md-icon>
        </md-navigation-tab>
        
        <md-navigation-tab 
          label="Чат" 
          data-href="/chat"
          active={pathname === '/chat'}
        >
          <md-icon slot="inactive-icon">chat</md-icon>
          <md-icon slot="active-icon">chat</md-icon>
        </md-navigation-tab>
      </md-navigation-bar>
    );
  }

  // Аналогично для client и parent...
  return null;
}

4. Список студентов

'use client';

export default function StudentsPage() {
  return (
    <div className="md-grid">
      <div className="md-col-4 md-col-sm-8 md-col-md-12">
        <md-list>
          <md-list-item>
            <md-icon slot="start">person</md-icon>
            <div slot="headline">Иван Иванов</div>
            <div slot="supporting-text">ivan@example.com</div>
            <md-icon-button slot="end">
              <md-icon>more_vert</md-icon>
            </md-icon-button>
          </md-list-item>
          
          <md-divider></md-divider>
          
          <md-list-item>
            <md-icon slot="start">person</md-icon>
            <div slot="headline">Петр Петров</div>
            <div slot="supporting-text">petr@example.com</div>
            <md-icon-button slot="end">
              <md-icon>more_vert</md-icon>
            </md-icon-button>
          </md-list-item>
        </md-list>
      </div>
    </div>
  );
}

5. Диалог (модальное окно)

'use client';

import { useRef } from 'react';

export function CreateLessonDialog() {
  const dialogRef = useRef<any>(null);

  const openDialog = () => {
    dialogRef.current?.show();
  };

  const closeDialog = () => {
    dialogRef.current?.close();
  };

  return (
    <>
      <md-filled-button onClick={openDialog}>
        Создать занятие
      </md-filled-button>

      <md-dialog ref={dialogRef}>
        <div slot="headline">Новое занятие</div>
        <form slot="content" method="dialog">
          <md-outlined-text-field
            label="Название"
            required
            style={{ width: '100%', marginBottom: '16px' }}
          ></md-outlined-text-field>
          
          <md-outlined-text-field
            label="Дата"
            type="date"
            required
            style={{ width: '100%', marginBottom: '16px' }}
          ></md-outlined-text-field>
        </form>
        <div slot="actions">
          <md-text-button onClick={closeDialog}>Отмена</md-text-button>
          <md-filled-button onClick={closeDialog}>Создать</md-filled-button>
        </div>
      </md-dialog>
    </>
  );
}

6. Chips (фильтры)

'use client';

export function FiltersBar() {
  return (
    <md-chip-set>
      <md-filter-chip label="Все занятия"></md-filter-chip>
      <md-filter-chip label="Активные"></md-filter-chip>
      <md-filter-chip label="Завершенные"></md-filter-chip>
      <md-filter-chip label="Отмененные"></md-filter-chip>
    </md-chip-set>
  );
}

7. Прогресс индикаторы

'use client';

export function LoadingIndicator() {
  return (
    <div style={{ display: 'flex', justifyContent: 'center', padding: '48px' }}>
      <md-circular-progress indeterminate></md-circular-progress>
    </div>
  );
}

export function UploadProgress({ value }: { value: number }) {
  return (
    <md-linear-progress value={value / 100}></md-linear-progress>
  );
}

🎨 Кастомизация под iOS 24+ стиль

Создать файл styles/ios-material.css:

/* iOS 24+ адаптация для Material компонентов */

/* Bottom Navigation Bar */
md-navigation-bar {
  --md-navigation-bar-container-color: rgba(255, 255, 255, 0.8);
  --md-navigation-bar-container-height: 80px;
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-top: 0.5px solid rgba(0, 0, 0, 0.1);
  box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.08);
  border-radius: 24px 24px 0 0;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1000;
}

[data-theme="dark"] md-navigation-bar {
  --md-navigation-bar-container-color: rgba(28, 28, 30, 0.9);
  border-top-color: rgba(255, 255, 255, 0.1);
}

/* Cards с blur эффектом */
md-elevated-card {
  --md-elevated-card-container-color: rgba(255, 255, 255, 0.8);
  --md-elevated-card-container-shape: 20px;
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}

[data-theme="dark"] md-elevated-card {
  --md-elevated-card-container-color: rgba(28, 28, 30, 0.8);
}

/* Buttons с iOS стилем */
md-filled-button {
  --md-filled-button-container-shape: 16px;
  --md-filled-button-container-height: 48px;
  font-weight: 500;
}

/* Text Fields с iOS стилем */
md-outlined-text-field {
  --md-outlined-text-field-container-shape: 12px;
  --md-outlined-text-field-outline-width: 1px;
}

/* List Items с отступами */
md-list-item {
  --md-list-item-container-shape: 12px;
  margin: 4px 8px;
}

/* Dialogs с rounded corners */
md-dialog {
  --md-dialog-container-shape: 24px;
}

🌈 Material Icons

Использовать Material Symbols из Google Fonts:

<!-- В app/layout.tsx или в head -->
<link 
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" 
  rel="stylesheet" 
/>

Использование:

<md-icon>home</md-icon>
<md-icon>calendar_month</md-icon>
<md-icon>chat</md-icon>
<md-icon>group</md-icon>
<md-icon>settings</md-icon>

Список популярных иконок:

  • home - Главная
  • calendar_month - Календарь
  • chat - Чат
  • group - Студенты/Группы
  • person - Профиль
  • settings - Настройки
  • notifications - Уведомления
  • payment - Оплата
  • school - Обучение
  • video_call - Видеозвонок
  • assignment - Задания
  • folder - Материалы

📋 Типографика Material Design 3

<!-- Заголовки -->
<h1 className="md-typescale-display-large">Display Large</h1>
<h2 className="md-typescale-display-medium">Display Medium</h2>
<h3 className="md-typescale-display-small">Display Small</h3>

<h1 className="md-typescale-headline-large">Headline Large</h1>
<h2 className="md-typescale-headline-medium">Headline Medium</h2>
<h3 className="md-typescale-headline-small">Headline Small</h3>

<!-- Основной текст -->
<p className="md-typescale-body-large">Body Large</p>
<p className="md-typescale-body-medium">Body Medium</p>
<p className="md-typescale-body-small">Body Small</p>

<!-- Подписи -->
<p className="md-typescale-label-large">Label Large</p>
<p className="md-typescale-label-medium">Label Medium</p>
<p className="md-typescale-label-small">Label Small</p>

Импортировать типографику:

import { styles as typescaleStyles } from '@material/web/typography/md-typescale-styles.js';

// В layout.tsx
useEffect(() => {
  document.adoptedStyleSheets.push(typescaleStyles.styleSheet);
}, []);

🎯 Важные замечания

  1. Web Components в React:

    • Material Web Components - это нативные Web Components
    • Используются напрямую в JSX как HTML элементы
    • Не нужны wrapper компоненты
  2. События:

    • Используйте ref для доступа к элементу
    • Добавляйте слушатели событий через addEventListener
  3. Стилизация:

    • CSS Variables для кастомизации
    • Только чистый CSS, без Tailwind
    • Material Grid System для layout
  4. Компоненты из labs:

    • Некоторые компоненты находятся в @material/web/labs/
    • Например: navigation-bar, badge, card

Документация: https://github.com/material-components/material-web
Demo: https://material-web.dev/