20 KiB
20 KiB
🎨 Руководство по 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);
}, []);
🎯 Важные замечания
-
Web Components в React:
- Material Web Components - это нативные Web Components
- Используются напрямую в JSX как HTML элементы
- Не нужны wrapper компоненты
-
События:
- Используйте
refдля доступа к элементу - Добавляйте слушатели событий через
addEventListener
- Используйте
-
Стилизация:
- CSS Variables для кастомизации
- Только чистый CSS, без Tailwind
- Material Grid System для layout
-
Компоненты из
labs:- Некоторые компоненты находятся в
@material/web/labs/ - Например:
navigation-bar,badge,card
- Некоторые компоненты находятся в
Документация: https://github.com/material-components/material-web
Demo: https://material-web.dev/