moobile
Deploy to Production / deploy-production (push) Successful in 27s Details

This commit is contained in:
root 2026-02-14 03:30:37 +03:00
parent 0b5fb434db
commit b4b99491ae
13 changed files with 3939 additions and 3415 deletions

View File

@ -1,64 +1,64 @@
import { AuthRedirect } from '@/components/auth/AuthRedirect'; import { AuthRedirect } from '@/components/auth/AuthRedirect';
export default function AuthLayout({ export default function AuthLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<AuthRedirect> <AuthRedirect>
<div <div
data-no-nav data-no-nav
style={{ style={{
minHeight: '100vh', minHeight: '100vh',
display: 'grid', display: 'grid',
gridTemplateColumns: '1fr minmax(0, 520px)', gridTemplateColumns: '1fr minmax(0, 520px)',
}} }}
> >
{/* Левая колонка — пустая, фон как у body */} {/* Левая колонка — пустая, фон как у body */}
<div <div
style={{ style={{
minHeight: '100vh', minHeight: '100vh',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}} }}
aria-hidden aria-hidden
> >
<img <img
src="/logo/logo.svg" src="/logo/logo.svg"
alt="Uchill Logo" alt="Uchill Logo"
style={{ style={{
width: '240px', width: '240px',
height: 'auto', height: 'auto',
opacity: 0.8 opacity: 0.8
}} }}
/> />
</div> </div>
{/* Правая колонка — форма на белом фоне */} {/* Правая колонка — форма на белом фоне */}
<div <div
style={{ style={{
minHeight: '100vh', minHeight: '100vh',
background: '#fff', background: '#fff',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
padding: '24px 32px', padding: '24px 32px',
boxSizing: 'border-box', boxSizing: 'border-box',
}} }}
> >
<div style={{ marginBottom: '32px', textAlign: 'center' }}> <div style={{ marginBottom: '32px', textAlign: 'center' }}>
<img <img
src="/logo/logo.svg" src="/logo/logo.svg"
alt="Uchill Logo" alt="Uchill Logo"
style={{ width: '120px', height: 'auto' }} style={{ width: '120px', height: 'auto' }}
/> />
</div> </div>
{children} {children}
</div> </div>
</div> </div>
</AuthRedirect> </AuthRedirect>
); );
} }

View File

@ -167,15 +167,14 @@ export default function ChatPage() {
}, [normalizeChat, refreshNavBadges]); }, [normalizeChat, refreshNavBadges]);
return ( return (
<div className="ios26-dashboard" style={{ padding: '16px' }}> <div className="ios26-dashboard ios26-chat-page" style={{ padding: '16px' }}>
<Box <Box
className="ios26-chat-layout"
sx={{ sx={{
display: 'grid', display: 'grid',
gridTemplateColumns: '320px 1fr', gridTemplateColumns: '320px 1fr',
gap: 'var(--ios26-spacing)', gap: 'var(--ios26-spacing)',
alignItems: 'stretch', alignItems: 'stretch',
// учитываем padding контейнера (16px сверху и снизу),
// чтобы итоговая высота блока была ~90vh
height: 'calc(90vh - 32px)', height: 'calc(90vh - 32px)',
maxHeight: 'calc(90vh - 32px)', maxHeight: 'calc(90vh - 32px)',
overflow: 'hidden', overflow: 'hidden',

View File

@ -1,6 +1,19 @@
'use client'; 'use client';
import { useEffect, useState, useCallback, Suspense } from 'react'; import { useEffect, useState, useCallback, Suspense } from 'react';
const MOBILE_BREAKPOINT = 767;
function useIsMobile() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const mq = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`);
setIsMobile(mq.matches);
const listener = () => setIsMobile(mq.matches);
mq.addEventListener('change', listener);
return () => mq.removeEventListener('change', listener);
}, []);
return isMobile;
}
import { useRouter, usePathname } from 'next/navigation'; import { useRouter, usePathname } from 'next/navigation';
import { BottomNavigationBar } from '@/components/navigation/BottomNavigationBar'; import { BottomNavigationBar } from '@/components/navigation/BottomNavigationBar';
import { TopNavigationBar } from '@/components/navigation/TopNavigationBar'; import { TopNavigationBar } from '@/components/navigation/TopNavigationBar';
@ -22,6 +35,7 @@ export default function ProtectedLayout({
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const { user, loading } = useAuth(); const { user, loading } = useAuth();
const isMobile = useIsMobile();
const [navBadges, setNavBadges] = useState<NavBadges | null>(null); const [navBadges, setNavBadges] = useState<NavBadges | null>(null);
const [subscriptionChecked, setSubscriptionChecked] = useState(false); const [subscriptionChecked, setSubscriptionChecked] = useState(false);
@ -139,25 +153,45 @@ export default function ProtectedLayout({
return ( return (
<NavBadgesProvider refreshNavBadges={refreshNavBadges}> <NavBadgesProvider refreshNavBadges={refreshNavBadges}>
<SelectedChildProvider> <SelectedChildProvider>
{!isFullWidthPage && <TopNavigationBar user={user} />} <div
<main className="protected-layout-root"
data-no-nav={isLiveKit ? true : undefined}
style={{ style={{
padding: isFullWidthPage ? '0' : '16px', display: 'flex',
maxWidth: isFullWidthPage ? '100%' : '1200px', flexDirection: 'column',
margin: isFullWidthPage ? '0' : '0 auto', minHeight: '100vh',
height: '100vh',
}} }}
> >
{children} {!isFullWidthPage && <TopNavigationBar user={user} />}
</main> <main
{!isLiveKit && ( className="protected-main"
<Suspense fallback={null}> data-no-nav={isLiveKit ? true : undefined}
<BottomNavigationBar userRole={user?.role} user={user} navBadges={navBadges} /> data-full-width={isFullWidthPage ? true : undefined}
</Suspense> style={{
)} flex: 1,
{!isLiveKit && user && ( minHeight: 0,
<NotificationBell /> overflow: 'auto',
)} padding: isFullWidthPage ? '0' : '16px',
maxWidth: isFullWidthPage ? '100%' : '1200px',
margin: isFullWidthPage ? '0' : '0 auto',
}}
>
{children}
</main>
{!isLiveKit && (
<Suspense fallback={null}>
<BottomNavigationBar
userRole={user?.role}
user={user}
navBadges={navBadges}
notificationsSlot={isMobile && user ? <NotificationBell embedded /> : null}
/>
</Suspense>
)}
{!isLiveKit && user && !isMobile && (
<NotificationBell />
)}
</div>
</SelectedChildProvider> </SelectedChildProvider>
</NavBadgesProvider> </NavBadgesProvider>
); );

View File

@ -381,6 +381,7 @@ function ProfilePage() {
return ( return (
<div <div
className="page-profile"
style={{ style={{
minHeight: '100vh', minHeight: '100vh',
padding: 24, padding: 24,
@ -389,6 +390,7 @@ function ProfilePage() {
}} }}
> >
<div <div
className="page-profile-grid"
style={{ style={{
display: 'grid', display: 'grid',
gridTemplateColumns: 'minmax(0, 440px) 1fr', gridTemplateColumns: 'minmax(0, 440px) 1fr',
@ -618,13 +620,7 @@ function ProfilePage() {
{/* Поля — 2 колонки */} {/* Поля — 2 колонки */}
<div style={{ padding: '0 24px 24px 24px' }}> <div style={{ padding: '0 24px 24px 24px' }}>
<div <div className="page-profile-fields" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 16px' }}>
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '12px 16px',
}}
>
<div> <div>
<label htmlFor="profile-first-name" style={{ display: 'block', fontSize: 12, fontWeight: 500, color: '#858585', marginBottom: 4 }}>Имя</label> <label htmlFor="profile-first-name" style={{ display: 'block', fontSize: 12, fontWeight: 500, color: '#858585', marginBottom: 4 }}>Имя</label>
<input <input

View File

@ -405,10 +405,10 @@ export default function SchedulePage() {
}; };
return ( return (
<div className="ios26-dashboard" style={{ padding: '16px' }}> <div className="ios26-dashboard ios26-schedule-page" style={{ padding: '16px' }}>
{error && <ErrorDisplay error={error} onRetry={loadLessons} />} {error && <ErrorDisplay error={error} onRetry={loadLessons} />}
<div style={{ <div className="ios26-schedule-layout" style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '5fr 2fr', gridTemplateColumns: '5fr 2fr',
gap: 'var(--ios26-spacing)', gap: 'var(--ios26-spacing)',
@ -417,44 +417,47 @@ export default function SchedulePage() {
// чтобы календарь не “схлопывался” на месяцах с меньшим количеством контента // чтобы календарь не “схлопывался” на месяцах с меньшим количеством контента
minHeight: 'calc(100vh - 160px)', minHeight: 'calc(100vh - 160px)',
}}> }}>
<Calendar <div className="ios26-schedule-calendar-wrap">
lessons={lessons} <Calendar
lessonsLoading={lessonsLoading} lessons={lessons}
lessonsLoading={lessonsLoading}
selectedDate={selectedDate} selectedDate={selectedDate}
onSelectSlot={handleSelectSlot} onSelectSlot={handleSelectSlot}
onSelectEvent={handleSelectEvent} onSelectEvent={handleSelectEvent}
onMonthChange={handleMonthChange} onMonthChange={handleMonthChange}
/> />
</div>
<CheckLesson <div className="ios26-schedule-right-wrap">
selectedDate={selectedDate} <CheckLesson
displayDate={displayDate} selectedDate={selectedDate}
lessonsLoading={lessonsLoading} displayDate={displayDate}
lessonsForSelectedDate={lessonsForSelectedDate} lessonsLoading={lessonsLoading}
isFormVisible={isFormVisible} lessonsForSelectedDate={lessonsForSelectedDate}
isMentor={isMentor} isFormVisible={isFormVisible}
onPrevDay={handlePrevDay} isMentor={isMentor}
onNextDay={handleNextDay} onPrevDay={handlePrevDay}
onAddLesson={handleAddLesson} onNextDay={handleNextDay}
onLessonClick={handleLessonClick} onAddLesson={handleAddLesson}
buttonComponentsLoaded={buttonComponentsLoaded} onLessonClick={handleLessonClick}
formComponentsLoaded={formComponentsLoaded} buttonComponentsLoaded={buttonComponentsLoaded}
lessonEditLoading={lessonEditLoading} formComponentsLoaded={formComponentsLoaded}
isEditingMode={isEditingMode} lessonEditLoading={lessonEditLoading}
formLoading={formLoading} isEditingMode={isEditingMode}
formError={formError} formLoading={formLoading}
formData={formData} formError={formError}
setFormData={setFormData} formData={formData}
selectedSubjectId={selectedSubjectId} setFormData={setFormData}
selectedMentorSubjectId={selectedMentorSubjectId} selectedSubjectId={selectedSubjectId}
onSubjectChange={handleSubjectChange} selectedMentorSubjectId={selectedMentorSubjectId}
students={students} onSubjectChange={handleSubjectChange}
subjects={subjects} students={students}
mentorSubjects={mentorSubjects} subjects={subjects}
onSubmit={handleSubmit} mentorSubjects={mentorSubjects}
onCancel={handleCancel} onSubmit={handleSubmit}
onDelete={isEditingMode ? handleDelete : undefined} onCancel={handleCancel}
/> onDelete={isEditingMode ? handleDelete : undefined}
/>
</div>
</div> </div>
</div> </div>
); );

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,8 @@ interface BottomNavigationBarProps {
userRole?: string; userRole?: string;
user?: User | null; user?: User | null;
navBadges?: NavBadges | null; navBadges?: NavBadges | null;
/** Слот для кнопки уведомлений (на мобильном — 4-й элемент в первом ряду). */
notificationsSlot?: React.ReactNode;
/** Выдвижная панель справа (3 колонки). При клике по пункту вызывается onClose. */ /** Выдвижная панель справа (3 колонки). При клике по пункту вызывается onClose. */
slideout?: boolean; slideout?: boolean;
onClose?: () => void; onClose?: () => void;
@ -57,7 +59,7 @@ function getBadgeCount(item: NavigationItem, navBadges: NavBadges | null | undef
} }
} }
export function BottomNavigationBar({ userRole, user, navBadges, slideout, onClose }: BottomNavigationBarProps) { export function BottomNavigationBar({ userRole, user, navBadges, notificationsSlot, slideout, onClose }: BottomNavigationBarProps) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@ -113,8 +115,8 @@ export function BottomNavigationBar({ userRole, user, navBadges, slideout, onClo
return common; return common;
}, [userRole]); }, [userRole]);
const firstRowItems = navigationItems.slice(0, 5); const firstRowItems = navigationItems.slice(0, notificationsSlot ? 3 : 5);
const restItems = navigationItems.slice(5); const restItems = navigationItems.slice(notificationsSlot ? 3 : 5);
const hasMore = restItems.length > 0; const hasMore = restItems.length > 0;
// Подсветка активного таба по текущему URL // Подсветка активного таба по текущему URL
@ -270,22 +272,32 @@ export function BottomNavigationBar({ userRole, user, navBadges, slideout, onClo
<div <div
className={ className={
'ios26-bottom-nav-first-row' + 'ios26-bottom-nav-first-row' +
(userRole === 'parent' ? ' ios26-bottom-nav-first-row--with-selector' : '') (userRole === 'parent' ? ' ios26-bottom-nav-first-row--with-selector' : '') +
(notificationsSlot ? ' ios26-bottom-nav-first-row--with-notifications' : '')
} }
> >
{userRole === 'parent' && <ChildSelectorCompact />} {userRole === 'parent' && <ChildSelectorCompact />}
{userRole === 'parent' ? ( {userRole === 'parent' ? (
<div className="ios26-bottom-nav-first-row-buttons"> <div
className={
'ios26-bottom-nav-first-row-buttons' +
(notificationsSlot ? ' ios26-bottom-nav-first-row-buttons--with-notifications' : '')
}
>
{firstRowItems.map((item, i) => renderButton(item, i))} {firstRowItems.map((item, i) => renderButton(item, i))}
{notificationsSlot}
</div> </div>
) : ( ) : (
firstRowItems.map((item, i) => renderButton(item, i)) <>
{firstRowItems.map((item, i) => renderButton(item, i))}
{notificationsSlot}
</>
)} )}
</div> </div>
<div <div
className={'ios26-bottom-nav-rest' + (expanded ? ' ios26-bottom-nav-rest--expanded' : '')} className={'ios26-bottom-nav-rest' + (expanded ? ' ios26-bottom-nav-rest--expanded' : '')}
> >
{restItems.map((item, i) => renderButton(item, 5 + i))} {restItems.map((item, i) => renderButton(item, (notificationsSlot ? 3 : 5) + i))}
</div> </div>
</div> </div>
</div> </div>

View File

@ -91,7 +91,7 @@ function NotificationItem({
const SCROLL_LOAD_MORE_THRESHOLD = 80; const SCROLL_LOAD_MORE_THRESHOLD = 80;
export function NotificationBell() { export function NotificationBell({ embedded }: { embedded?: boolean }) {
const refreshNavBadges = useNavBadgesRefresh(); const refreshNavBadges = useNavBadgesRefresh();
const { const {
list, list,
@ -164,16 +164,26 @@ export function NotificationBell() {
<div <div
data-notification-bell data-notification-bell
style={{ style={
position: 'fixed', embedded
right: BELL_POSITION.right, ? {
bottom: BELL_POSITION.bottom, position: 'relative',
zIndex: 9998, display: 'flex',
display: 'flex', alignItems: 'center',
alignItems: 'flex-end', justifyContent: 'center',
justifyContent: 'flex-end', flexDirection: 'column',
flexDirection: 'column', }
}} : {
position: 'fixed',
right: BELL_POSITION.right,
bottom: BELL_POSITION.bottom,
zIndex: 9998,
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'flex-end',
flexDirection: 'column',
}
}
> >
{/* Панель уведомлений — выезжает справа от колокольчика */} {/* Панель уведомлений — выезжает справа от колокольчика */}
{open && ( {open && (
@ -182,8 +192,9 @@ export function NotificationBell() {
className="notification-panel-enter-active" className="notification-panel-enter-active"
style={{ style={{
position: 'absolute', position: 'absolute',
right: 52, ...(embedded
bottom: 0, ? { bottom: '100%', marginBottom: 8, left: '50%', transform: 'translateX(-50%)' }
: { right: 52, bottom: 0 }),
width: PANEL_WIDTH, width: PANEL_WIDTH,
maxHeight: PANEL_MAX_HEIGHT, maxHeight: PANEL_MAX_HEIGHT,
backgroundColor: 'var(--md-sys-color-surface)', backgroundColor: 'var(--md-sys-color-surface)',
@ -295,56 +306,97 @@ export function NotificationBell() {
</div> </div>
)} )}
{/* Кнопка-колокольчик */} {/* Кнопка-колокольчик: в меню — как пункт навигации, иначе — круглая */}
<button {embedded ? (
type="button" <button
aria-label={unreadCount > 0 ? `Уведомления: ${unreadCount} непрочитанных` : 'Уведомления'} type="button"
onClick={() => setOpen((o) => !o)} className="ios26-bottom-nav-button"
style={{ aria-label={unreadCount > 0 ? `Уведомления: ${unreadCount} непрочитанных` : 'Уведомления'}
position: 'relative', onClick={() => setOpen((o) => !o)}
width: 48, >
height: 48, <span style={{ position: 'relative', display: 'inline-flex' }}>
borderRadius: '50%', <span className="material-symbols-outlined ios26-bottom-nav-icon">
border: 'none', notifications
background: 'var(--md-sys-color-primary-container)', </span>
color: 'var(--md-sys-color-primary)', {unreadCount > 0 && (
cursor: 'pointer', <span
display: 'flex', className="ios26-bottom-nav-badge"
alignItems: 'center', style={{
justifyContent: 'center', position: 'absolute',
boxShadow: 'var(--ios-shadow-soft)', top: -8,
}} right: -16,
> minWidth: 18,
<span className="material-symbols-outlined" style={{ fontSize: 24 }}> height: 18,
notifications borderRadius: 9,
</span> background: 'var(--md-sys-color-error, #b3261e)',
{unreadCount > 0 && ( color: '#fff',
<span fontSize: 11,
style={{ fontWeight: 600,
position: 'absolute', display: 'flex',
top: -2, alignItems: 'center',
right: -2, justifyContent: 'center',
minWidth: 18, padding: '0 4px',
height: 18, boxSizing: 'border-box',
padding: '0 5px', }}
borderRadius: 9, title={`${unreadCount} непрочитанных`}
backgroundColor: 'var(--md-sys-color-error, #c00)', >
color: '#fff', {unreadCount > 99 ? '99+' : unreadCount}
fontSize: 11, </span>
fontWeight: 700, )}
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
maxWidth: 48,
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
title={`${unreadCount} непрочитанных`}
>
{unreadCount}
</span> </span>
)} <span className="ios26-bottom-nav-label">Уведомления</span>
</button> </button>
) : (
<button
type="button"
aria-label={unreadCount > 0 ? `Уведомления: ${unreadCount} непрочитанных` : 'Уведомления'}
onClick={() => setOpen((o) => !o)}
style={{
position: 'relative',
width: 48,
height: 48,
borderRadius: '50%',
border: 'none',
background: 'var(--md-sys-color-primary-container)',
color: 'var(--md-sys-color-primary)',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: 'var(--ios-shadow-soft)',
}}
>
<span className="material-symbols-outlined" style={{ fontSize: 24 }}>
notifications
</span>
{unreadCount > 0 && (
<span
style={{
position: 'absolute',
top: -2,
right: -2,
minWidth: 18,
height: 18,
padding: '0 5px',
borderRadius: 9,
backgroundColor: 'var(--md-sys-color-error, #c00)',
color: '#fff',
fontSize: 11,
fontWeight: 700,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
maxWidth: 48,
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
title={`${unreadCount} непрочитанных`}
>
{unreadCount}
</span>
)}
</button>
)}
</div> </div>
</> </>
); );

View File

@ -188,15 +188,21 @@ export function ProfilePaymentTab() {
</ul> </ul>
<div className="ios26-plan-card__actions"> <div className="ios26-plan-card__actions">
{free ? ( {free ? (
<button subscription ? (
type="button" <span className="ios26-plan-card__action" style={{ opacity: 0.8, cursor: 'default' }}>
className="ios26-plan-card__action" Подписка уже активирована
onClick={() => handleActivateFree(plan)} </span>
disabled={!!activatingPlanId} ) : (
style={{ cursor: activatingPlanId ? 'wait' : 'pointer' }} <button
> type="button"
{activatingPlanId === plan.id ? 'Активация...' : 'Активировать'} className="ios26-plan-card__action"
</button> onClick={() => handleActivateFree(plan)}
disabled={!!activatingPlanId}
style={{ cursor: activatingPlanId ? 'wait' : 'pointer' }}
>
{activatingPlanId === plan.id ? 'Активация...' : 'Активировать'}
</button>
)
) : ( ) : (
<Link href="/payment" className="ios26-plan-card__action"> <Link href="/payment" className="ios26-plan-card__action">
Подробнее и оплатить Подробнее и оплатить

View File

@ -1,17 +1,26 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { getReferralProfile, getReferralStats } from '@/api/referrals'; import { getReferralProfile, getReferralStats, getMyReferrals, type MyReferralItem } from '@/api/referrals';
import { LoadingSpinner } from '@/components/common/LoadingSpinner'; import { LoadingSpinner } from '@/components/common/LoadingSpinner';
import { useToast } from '@/contexts/ToastContext'; import { useToast } from '@/contexts/ToastContext';
const formatCurrency = (v: number) => const formatCurrency = (v: number) =>
new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format(v); new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format(v);
function formatDate(s: string) {
try {
return new Date(s).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
} catch {
return s;
}
}
export function ReferralsPageContent() { export function ReferralsPageContent() {
const { showToast } = useToast(); const { showToast } = useToast();
const [profile, setProfile] = useState<any>(null); const [profile, setProfile] = useState<any>(null);
const [stats, setStats] = useState<any>(null); const [stats, setStats] = useState<any>(null);
const [referralsList, setReferralsList] = useState<{ direct: MyReferralItem[]; indirect: MyReferralItem[] } | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@ -19,6 +28,7 @@ export function ReferralsPageContent() {
Promise.all([ Promise.all([
getReferralProfile().then(setProfile), getReferralProfile().then(setProfile),
getReferralStats().then(setStats), getReferralStats().then(setStats),
getMyReferrals().then(setReferralsList).catch(() => setReferralsList({ direct: [], indirect: [] })),
]) ])
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, []);
@ -138,6 +148,58 @@ export function ReferralsPageContent() {
</div> </div>
</div> </div>
)} )}
{/* Список приглашённых рефералов */}
{referralsList && (referralsList.direct.length > 0 || referralsList.indirect.length > 0) && (
<div>
<div
style={{
fontSize: 11,
fontWeight: 600,
letterSpacing: '0.05em',
color: 'var(--md-sys-color-on-surface-variant)',
marginBottom: 8,
}}
>
ПРИГЛАШЁННЫЕ
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{referralsList.direct.length > 0 && (
<div>
<div style={{ fontSize: 12, color: 'var(--md-sys-color-on-surface-variant)', marginBottom: 6 }}>
Прямые рефералы ({referralsList.direct.length})
</div>
<ul style={{ margin: 0, paddingLeft: 20, listStyle: 'disc' }}>
{referralsList.direct.map((r: MyReferralItem, i: number) => (
<li key={i} style={{ marginBottom: 4, fontSize: 14, color: 'var(--md-sys-color-on-surface)' }}>
{r.email} {r.level}, {r.total_points} баллов, зарегистрирован {formatDate(r.created_at)}
</li>
))}
</ul>
</div>
)}
{referralsList.indirect.length > 0 && (
<div>
<div style={{ fontSize: 12, color: 'var(--md-sys-color-on-surface-variant)', marginBottom: 6 }}>
Рефералы ваших рефералов ({referralsList.indirect.length})
</div>
<ul style={{ margin: 0, paddingLeft: 20, listStyle: 'disc' }}>
{referralsList.indirect.map((r: MyReferralItem, i: number) => (
<li key={i} style={{ marginBottom: 4, fontSize: 14, color: 'var(--md-sys-color-on-surface)' }}>
{r.email} {r.level}, {r.total_points} баллов, зарегистрирован {formatDate(r.created_at)}
</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
{referralsList && referralsList.direct.length === 0 && referralsList.indirect.length === 0 && (
<p style={{ fontSize: 14, color: 'var(--md-sys-color-on-surface-variant)' }}>
Пока никого нет. Поделитесь реферальной ссылкой когда кто-то зарегистрируется по ней, он появится здесь и вы получите уведомление.
</p>
)}
</div> </div>
); );
} }

View File

@ -169,6 +169,10 @@ body:has([data-no-nav]) {
padding-bottom: 0; padding-bottom: 0;
} }
body:has(.protected-layout-root) {
padding-bottom: 0;
}
body > * { body > * {
position: relative; position: relative;
z-index: 1; z-index: 1;
@ -270,10 +274,10 @@ img {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
} }
/* Кастомный нижний бар iOS 26 — первый ряд всегда, остальное по раскрытию */ /* Кастомный нижний бар iOS 26 — первый ряд всегда, остальное по раскрытию. Ноутбук и выше: fixed, bottom 20px */
.ios26-bottom-nav-container { .ios26-bottom-nav-container {
position: fixed; position: fixed;
bottom: 30px; bottom: 20px;
left: 16px; left: 16px;
right: 16px; right: 16px;
z-index: 1000; z-index: 1000;
@ -294,6 +298,70 @@ img {
padding-bottom: env(safe-area-inset-bottom, 0); padding-bottom: env(safe-area-inset-bottom, 0);
} }
/* Protected layout: контент скроллится сверху, снизу меню. На мобильном — меню в потоке; ноутбук+ — fixed */
.protected-layout-root {
display: flex;
flex-direction: column;
min-height: 100vh;
height: 100vh;
}
.protected-layout-root .protected-main {
flex: 1;
min-height: 0;
overflow: auto;
}
/* Ноутбук и выше (768px+): нижний бар fixed, bottom 20px, контенту отступ снизу */
@media (min-width: 768px) {
.protected-layout-root .ios26-bottom-nav-container {
position: fixed;
bottom: 20px;
left: 16px;
right: 16px;
margin: 0 auto;
max-width: min(900px, 100%);
}
.protected-layout-root .protected-main {
padding-bottom: 88px;
}
}
/* Мобильный: меню в потоке, на всю ширину, прижато к низу */
@media (max-width: 767px) {
.protected-layout-root .ios26-bottom-nav-container {
position: relative;
bottom: auto;
left: 0;
right: 0;
margin: 0;
max-width: 100%;
flex-shrink: 0;
border-radius: 0;
}
.protected-layout-root .ios26-bottom-nav {
border-radius: 0;
border-left: none;
border-right: none;
padding-bottom: calc(8px + env(safe-area-inset-bottom, 0));
}
/* Все строки навигации по 4 элемента на мобильном */
.protected-layout-root .ios26-bottom-nav-first-row {
grid-template-columns: repeat(4, 1fr);
}
.protected-layout-root .ios26-bottom-nav-first-row-buttons {
grid-template-columns: repeat(4, 1fr);
}
.protected-layout-root .ios26-bottom-nav-rest {
grid-template-columns: repeat(4, 1fr);
}
}
.ios26-bottom-nav-expand-trigger { .ios26-bottom-nav-expand-trigger {
all: unset; all: unset;
cursor: pointer; cursor: pointer;
@ -353,6 +421,15 @@ img {
grid-template-columns: unset; grid-template-columns: unset;
} }
/* Первый ряд: 4 колонки, когда есть слот уведомлений (мобильное меню) */
.ios26-bottom-nav-first-row--with-notifications {
grid-template-columns: repeat(4, 1fr);
}
.ios26-bottom-nav-first-row-buttons--with-notifications {
grid-template-columns: repeat(4, 1fr);
}
.ios26-bottom-nav-first-row-buttons { .ios26-bottom-nav-first-row-buttons {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@ -1306,6 +1383,308 @@ img {
font-size: 15px !important; font-size: 15px !important;
} }
/* ========== Адаптив: планшет и телефон (dashboard, chat, materials, homework, my-progress, request-mentor, profile, livekit, students, feedback, analytics, payment, referrals) ========== */
/* Планшет: 768px — 1024px */
@media (max-width: 1024px) {
.protected-main {
padding-left: 12px !important;
padding-right: 12px !important;
padding-top: 12px !important;
padding-bottom: 12px !important;
}
.protected-main[data-full-width] {
padding: 12px !important;
}
.ios26-dashboard {
padding: 12px;
}
.ios26-dashboard.ios26-dashboard-grid {
gap: 12px;
}
.ios26-stat-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ios26-analytics-chart-row {
flex-direction: column;
}
.ios26-analytics-two-cols {
flex-direction: column;
}
.ios26-dashboard-analytics .ios26-stat-grid--aside {
width: 100%;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
.ios26-dashboard-analytics .ios26-stat-grid--aside .ios26-stat-tile {
flex: 1;
min-width: 140px;
}
.ios26-bottom-nav-container {
left: 8px;
right: 8px;
}
.ios26-panel {
padding: 12px;
}
}
/* Телефон: до 767px */
@media (max-width: 767px) {
body {
padding-bottom: calc(50px + 52px);
}
.protected-main {
padding-left: 10px !important;
padding-right: 10px !important;
padding-top: 10px !important;
padding-bottom: 10px !important;
}
.protected-main[data-full-width] {
padding: 10px !important;
}
.ios26-dashboard {
padding: 10px;
}
.ios26-dashboard.ios26-dashboard-grid {
grid-template-columns: 1fr;
gap: 10px;
}
.ios26-stat-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.ios26-stat-tile {
padding: 10px 12px;
}
.ios26-stat-label,
.ios26-stat-value {
font-size: 15px;
}
.ios26-section-title {
font-size: 18px;
margin-bottom: 12px;
}
.ios26-bottom-nav-container {
left: 6px;
right: 6px;
bottom: 20px;
}
.ios26-bottom-nav {
padding: 6px 8px;
}
.ios26-bottom-nav-first-row {
gap: 2px 4px;
}
.ios26-bottom-nav-rest--expanded {
max-height: 200px;
}
.ios26-panel {
padding: 10px;
border-radius: var(--ios26-radius-sm);
}
.ios26-dashboard-analytics .ios26-analytics-chart,
.ios26-dashboard-analytics .ios26-analytics-chart-placeholder {
min-height: 60vh;
}
.ios26-analytics-nav .ios26-analytics-nav-btn:first-child {
left: 8px;
}
.ios26-analytics-nav .ios26-analytics-nav-btn:last-child {
right: 8px;
}
.ios26-feedback-page {
padding: 10px;
}
.ios26-feedback-kanban {
gap: 12px;
}
.ios26-list-row {
font-size: 15px;
padding: 10px 0;
}
/* Страница Студенты: сетка карточек и боковая панель */
.page-students {
padding: 16px !important;
}
.students-cards-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
align-items: start;
}
.students-side-panel {
width: 100% !important;
max-width: 100vw;
right: 0 !important;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
/* Страница Профиль: одна колонка, поля в 1 колонку */
.page-profile {
padding: 16px !important;
}
.page-profile-grid {
grid-template-columns: 1fr !important;
}
.page-profile-fields {
grid-template-columns: 1fr !important;
}
/* Аналитика: уже есть .ios26-analytics-chart-row column, .ios26-analytics-two-cols column */
}
/* Маленький телефон: до 480px */
@media (max-width: 480px) {
.protected-main {
padding-left: 8px !important;
padding-right: 8px !important;
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.protected-main[data-full-width] {
padding: 8px !important;
}
.page-students {
padding: 12px !important;
}
.students-cards-grid {
grid-template-columns: 1fr;
gap: 10px;
}
.page-profile {
padding: 12px !important;
}
.ios26-dashboard {
padding: 8px;
}
.ios26-dashboard.ios26-dashboard-grid {
gap: 8px;
}
.ios26-stat-grid {
grid-template-columns: 1fr;
}
.ios26-stat-tile {
padding: 12px;
}
.ios26-bottom-nav-container {
left: 4px;
right: 4px;
bottom: 16px;
}
.ios26-bottom-nav-first-row {
grid-template-columns: repeat(5, 1fr);
}
.ios26-bottom-nav-button {
min-width: 0;
}
.ios26-bottom-nav-label {
font-size: 10px;
}
.ios26-panel {
padding: 8px;
}
.ios26-section-title {
font-size: 17px;
}
}
/* Страница Студенты: сетка карточек (десктоп) */
.students-cards-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 16px;
align-items: start;
}
/* Страница календаря (расписание): планшет — правая часть сверху, календарь снизу; телефон — только правая часть */
@media (max-width: 1024px) {
.ios26-schedule-layout {
grid-template-columns: 1fr !important;
grid-template-rows: auto 1fr;
}
.ios26-schedule-calendar-wrap {
order: 2;
}
.ios26-schedule-right-wrap {
order: 1;
}
}
@media (max-width: 767px) {
.ios26-schedule-calendar-wrap {
display: none !important;
}
.ios26-schedule-layout {
grid-template-rows: 1fr !important;
min-height: auto !important;
}
}
/* Chat: список + окно чата — на планшете и телефоне одна колонка, список сверху */
@media (max-width: 900px) {
.ios26-chat-page {
padding: 10px !important;
}
.ios26-chat-layout {
grid-template-columns: 1fr !important;
grid-template-rows: auto 1fr;
height: calc(100vh - 120px) !important;
max-height: none !important;
}
.ios26-chat-layout > div:first-of-type {
max-height: 38vh;
min-height: 180px;
overflow: auto;
}
}
@media (max-width: 480px) {
.ios26-chat-page {
padding: 8px !important;
}
.ios26-chat-layout {
height: calc(100vh - 100px) !important;
}
.ios26-chat-layout > div:first-of-type {
max-height: 35vh;
min-height: 160px;
}
}
/* Materials / Homework / Students / Request-mentor / Profile: карточки и контент */
@media (max-width: 767px) {
.ios26-payment-tab,
.ios26-plan-card-grid {
gap: 10px;
}
.ios26-plan-card {
padding: 12px;
}
.protected-main md-elevated-card {
padding: 20px !important;
border-radius: 16px !important;
}
}
@media (max-width: 480px) {
.protected-main md-elevated-card {
padding: 14px !important;
border-radius: 14px !important;
}
}
/* LiveKit: полноэкранный контейнер на мобильных */
@media (max-width: 767px) {
[data-lk-theme] {
font-size: 14px;
}
.protected-main[data-no-nav] {
padding: 0 !important;
}
}
/* Flip-карточка эффект */ /* Flip-карточка эффект */
.flip-card { .flip-card {
position: relative; position: relative;

View File

@ -1,429 +1,429 @@
/** /**
* Кастомизация LiveKit через CSS переменные. * Кастомизация LiveKit через CSS переменные.
* Все стили и скрипты LiveKit отдаются с нашего сервера (бандл + этот файл). * Все стили и скрипты LiveKit отдаются с нашего сервера (бандл + этот файл).
*/ */
@keyframes lk-spin { @keyframes lk-spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
:root { :root {
/* Цвета фона */ /* Цвета фона */
--lk-bg: #1a1a1a; --lk-bg: #1a1a1a;
--lk-bg2: #2a2a2a; --lk-bg2: #2a2a2a;
--lk-bg3: #3a3a3a; --lk-bg3: #3a3a3a;
/* Цвета текста */ /* Цвета текста */
--lk-fg: #ffffff; --lk-fg: #ffffff;
--lk-fg2: rgba(255, 255, 255, 0.7); --lk-fg2: rgba(255, 255, 255, 0.7);
/* Основные цвета */ /* Основные цвета */
--lk-control-bg: var(--md-sys-color-primary); --lk-control-bg: var(--md-sys-color-primary);
--lk-control-hover-bg: var(--md-sys-color-primary-container); --lk-control-hover-bg: var(--md-sys-color-primary-container);
--lk-button-bg: rgba(255, 255, 255, 0.15); --lk-button-bg: rgba(255, 255, 255, 0.15);
--lk-button-hover-bg: rgba(255, 255, 255, 0.25); --lk-button-hover-bg: rgba(255, 255, 255, 0.25);
/* Границы */ /* Границы */
--lk-border-color: rgba(255, 255, 255, 0.1); --lk-border-color: rgba(255, 255, 255, 0.1);
--lk-border-radius: 12px; --lk-border-radius: 12px;
/* Фокус */ /* Фокус */
--lk-focus-ring: var(--md-sys-color-primary); --lk-focus-ring: var(--md-sys-color-primary);
/* Ошибки */ /* Ошибки */
--lk-danger: var(--md-sys-color-error); --lk-danger: var(--md-sys-color-error);
/* Размеры */ /* Размеры */
--lk-control-bar-height: 80px; --lk-control-bar-height: 80px;
--lk-participant-tile-gap: 12px; --lk-participant-tile-gap: 12px;
} }
/* Панель управления — без ограничения по ширине */ /* Панель управления — без ограничения по ширине */
.lk-control-bar { .lk-control-bar {
background: rgba(0, 0, 0, 0.8) !important; background: rgba(0, 0, 0, 0.8) !important;
backdrop-filter: blur(20px) !important; backdrop-filter: blur(20px) !important;
border-radius: 16px !important; border-radius: 16px !important;
padding: 12px 16px !important; padding: 12px 16px !important;
margin: 16px !important; margin: 16px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important;
max-width: none !important; max-width: none !important;
width: auto !important; width: auto !important;
} }
.lk-control-bar .lk-button-group, .lk-control-bar .lk-button-group,
.lk-control-bar .lk-button-group-menu { .lk-control-bar .lk-button-group-menu {
max-width: none !important; max-width: none !important;
width: auto !important; width: auto !important;
} }
/* Кнопки управления — ширина по контенту, без жёсткого ограничения */ /* Кнопки управления — ширина по контенту, без жёсткого ограничения */
.lk-control-bar .lk-button { .lk-control-bar .lk-button {
min-width: 48px !important; min-width: 48px !important;
width: auto !important; width: auto !important;
height: 48px !important; height: 48px !important;
border-radius: 12px !important; border-radius: 12px !important;
transition: all 0.2s ease !important; transition: all 0.2s ease !important;
padding-left: 12px !important; padding-left: 12px !important;
padding-right: 12px !important; padding-right: 12px !important;
} }
/* Русские подписи: скрываем английский текст, показываем свой */ /* Русские подписи: скрываем английский текст, показываем свой */
.lk-control-bar .lk-button[data-lk-source="microphone"], .lk-control-bar .lk-button[data-lk-source="microphone"],
.lk-control-bar .lk-button[data-lk-source="camera"], .lk-control-bar .lk-button[data-lk-source="camera"],
.lk-control-bar .lk-button[data-lk-source="screen_share"], .lk-control-bar .lk-button[data-lk-source="screen_share"],
.lk-control-bar .lk-chat-toggle, .lk-control-bar .lk-chat-toggle,
.lk-control-bar .lk-disconnect-button, .lk-control-bar .lk-disconnect-button,
.lk-control-bar .lk-start-audio-button { .lk-control-bar .lk-start-audio-button {
font-size: 0 !important; font-size: 0 !important;
} }
.lk-control-bar .lk-button[data-lk-source="microphone"] > svg, .lk-control-bar .lk-button[data-lk-source="microphone"] > svg,
.lk-control-bar .lk-button[data-lk-source="camera"] > svg, .lk-control-bar .lk-button[data-lk-source="camera"] > svg,
.lk-control-bar .lk-button[data-lk-source="screen_share"] > svg, .lk-control-bar .lk-button[data-lk-source="screen_share"] > svg,
.lk-control-bar .lk-chat-toggle > svg, .lk-control-bar .lk-chat-toggle > svg,
.lk-control-bar .lk-disconnect-button > svg { .lk-control-bar .lk-disconnect-button > svg {
width: 16px !important; width: 16px !important;
height: 16px !important; height: 16px !important;
flex-shrink: 0 !important; flex-shrink: 0 !important;
} }
.lk-control-bar .lk-button[data-lk-source="microphone"]::after { .lk-control-bar .lk-button[data-lk-source="microphone"]::after {
content: "Микрофон"; content: "Микрофон";
font-size: 1rem; font-size: 1rem;
} }
.lk-control-bar .lk-button[data-lk-source="camera"]::after { .lk-control-bar .lk-button[data-lk-source="camera"]::after {
content: "Камера"; content: "Камера";
font-size: 1rem; font-size: 1rem;
} }
.lk-control-bar .lk-button[data-lk-source="screen_share"]::after { .lk-control-bar .lk-button[data-lk-source="screen_share"]::after {
content: "Поделиться экраном"; content: "Поделиться экраном";
font-size: 1rem; font-size: 1rem;
} }
.lk-control-bar .lk-button[data-lk-source="screen_share"][data-lk-enabled="true"]::after { .lk-control-bar .lk-button[data-lk-source="screen_share"][data-lk-enabled="true"]::after {
content: "Остановить демонстрацию"; content: "Остановить демонстрацию";
} }
.lk-control-bar .lk-chat-toggle::after { .lk-control-bar .lk-chat-toggle::after {
content: "Чат"; content: "Чат";
font-size: 1rem; font-size: 1rem;
} }
/* Кнопка бургер слева от микрофона — в панели LiveKit */ /* Кнопка бургер слева от микрофона — в панели LiveKit */
.lk-burger-button { .lk-burger-button {
background: rgba(255, 255, 255, 0.15) !important; background: rgba(255, 255, 255, 0.15) !important;
color: #fff !important; color: #fff !important;
} }
/* Скрываем стандартную кнопку «Выйти» — используем свою внутри панели (модалка: Выйти / Выйти и завершить занятие) */ /* Скрываем стандартную кнопку «Выйти» — используем свою внутри панели (модалка: Выйти / Выйти и завершить занятие) */
.lk-control-bar .lk-disconnect-button { .lk-control-bar .lk-disconnect-button {
display: none !important; display: none !important;
} }
.lk-control-bar .lk-disconnect-button::after { .lk-control-bar .lk-disconnect-button::after {
content: "Выйти"; content: "Выйти";
font-size: 1rem; font-size: 1rem;
} }
/* Наша кнопка «Выйти» — внутри панели, рядом с «Поделиться экраном» */ /* Наша кнопка «Выйти» — внутри панели, рядом с «Поделиться экраном» */
.lk-control-bar .lk-custom-exit-button { .lk-control-bar .lk-custom-exit-button {
font-size: 0 !important; font-size: 0 !important;
background: var(--md-sys-color-error) !important; background: var(--md-sys-color-error) !important;
color: #fff !important; color: #fff !important;
border: none; border: none;
cursor: pointer; cursor: pointer;
display: inline-flex !important; display: inline-flex !important;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.lk-control-bar .lk-custom-exit-button::after { .lk-control-bar .lk-custom-exit-button::after {
content: "Выйти"; content: "Выйти";
font-size: 1rem; font-size: 1rem;
} }
.lk-control-bar .lk-custom-exit-button > .material-symbols-outlined { .lk-control-bar .lk-custom-exit-button > .material-symbols-outlined {
color: #fff !important; color: #fff !important;
} }
/* Скрываем кнопку «Начать видео» — у нас свой StartAudioOverlay */ /* Скрываем кнопку «Начать видео» — у нас свой StartAudioOverlay */
.lk-control-bar .lk-start-audio-button { .lk-control-bar .lk-start-audio-button {
display: none !important; display: none !important;
} }
/* Кнопки без текста (только иконка) — минимальный размер */ /* Кнопки без текста (только иконка) — минимальный размер */
.lk-button { .lk-button {
min-width: 48px !important; min-width: 48px !important;
width: auto !important; width: auto !important;
height: 48px !important; height: 48px !important;
border-radius: 12px !important; border-radius: 12px !important;
transition: all 0.2s ease !important; transition: all 0.2s ease !important;
} }
.lk-button:hover { .lk-button:hover {
transform: scale(1.05); transform: scale(1.05);
} }
.lk-button:active { .lk-button:active {
transform: scale(0.95); transform: scale(0.95);
} }
/* Активная кнопка */ /* Активная кнопка */
.lk-button[data-lk-enabled="true"] { .lk-button[data-lk-enabled="true"] {
background: var(--md-sys-color-primary) !important; background: var(--md-sys-color-primary) !important;
} }
/* Кнопка отключения — белые иконка и текст */ /* Кнопка отключения — белые иконка и текст */
.lk-disconnect-button { .lk-disconnect-button {
background: var(--md-sys-color-error) !important; background: var(--md-sys-color-error) !important;
color: #fff !important; color: #fff !important;
} }
.lk-disconnect-button > svg { .lk-disconnect-button > svg {
color: #fff !important; color: #fff !important;
fill: currentColor; fill: currentColor;
} }
/* Плитки участников */ /* Плитки участников */
.lk-participant-tile { .lk-participant-tile {
border-radius: 12px !important; border-radius: 12px !important;
overflow: hidden !important; overflow: hidden !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
} }
/* Плейсхолдер без камеры: скрываем дефолтную SVG, показываем аватар из API */ /* Плейсхолдер без камеры: скрываем дефолтную SVG, показываем аватар из API */
.lk-participant-tile .lk-participant-placeholder svg { .lk-participant-tile .lk-participant-placeholder svg {
display: none !important; display: none !important;
} }
/* Контейнер для аватара — нужен для container queries */ /* Контейнер для аватара — нужен для container queries */
.lk-participant-tile .lk-participant-placeholder { .lk-participant-tile .lk-participant-placeholder {
container-type: size; container-type: size;
} }
.lk-participant-tile .lk-participant-placeholder img.lk-participant-avatar-img { .lk-participant-tile .lk-participant-placeholder img.lk-participant-avatar-img {
/* Квадрат: меньшая сторона контейнера, максимум 400px */ /* Квадрат: меньшая сторона контейнера, максимум 400px */
--avatar-size: min(min(80cqw, 80cqh), 400px); --avatar-size: min(min(80cqw, 80cqh), 400px);
width: var(--avatar-size); width: var(--avatar-size);
height: var(--avatar-size); height: var(--avatar-size);
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
object-fit: cover; object-fit: cover;
object-position: center; object-position: center;
border-radius: 50%; border-radius: 50%;
} }
/* Fallback для браузеров без container queries */ /* Fallback для браузеров без container queries */
@supports not (width: 1cqw) { @supports not (width: 1cqw) {
.lk-participant-tile .lk-participant-placeholder img.lk-participant-avatar-img { .lk-participant-tile .lk-participant-placeholder img.lk-participant-avatar-img {
width: 200px; width: 200px;
height: 200px; height: 200px;
} }
} }
/* Имя участника — белый текст (Камера, PiP) */ /* Имя участника — белый текст (Камера, PiP) */
.lk-participant-name { .lk-participant-name {
background: rgba(0, 0, 0, 0.7) !important; background: rgba(0, 0, 0, 0.7) !important;
backdrop-filter: blur(10px) !important; backdrop-filter: blur(10px) !important;
border-radius: 8px !important; border-radius: 8px !important;
padding: 6px 12px !important; padding: 6px 12px !important;
font-weight: 600 !important; font-weight: 600 !important;
color: #fff !important; color: #fff !important;
} }
/* Чат LiveKit скрыт — используем чат сервиса (платформы) */ /* Чат LiveKit скрыт — используем чат сервиса (платформы) */
.lk-video-conference .lk-chat { .lk-video-conference .lk-chat {
display: none !important; display: none !important;
} }
.lk-control-bar .lk-chat-toggle { .lk-control-bar .lk-chat-toggle {
display: none !important; display: none !important;
} }
/* Стили чата платформы оставляем для других страниц */ /* Стили чата платформы оставляем для других страниц */
.lk-chat { .lk-chat {
background: var(--md-sys-color-surface) !important; background: var(--md-sys-color-surface) !important;
border-left: 1px solid var(--md-sys-color-outline) !important; border-left: 1px solid var(--md-sys-color-outline) !important;
} }
.lk-chat-entry { .lk-chat-entry {
background: var(--md-sys-color-surface-container) !important; background: var(--md-sys-color-surface-container) !important;
border-radius: 12px !important; border-radius: 12px !important;
padding: 12px !important; padding: 12px !important;
margin-bottom: 12px !important; margin-bottom: 12px !important;
} }
/* Сетка участников */ /* Сетка участников */
.lk-grid-layout { .lk-grid-layout {
gap: 12px !important; gap: 12px !important;
padding: 12px !important; padding: 12px !important;
} }
/* Меню выбора устройств — без ограничения по ширине */ /* Меню выбора устройств — без ограничения по ширине */
.lk-device-menu, .lk-device-menu,
.lk-media-device-select { .lk-media-device-select {
max-width: none !important; max-width: none !important;
width: max-content !important; width: max-content !important;
min-width: 0 !important; min-width: 0 !important;
} }
.lk-media-device-select { .lk-media-device-select {
background: rgba(0, 0, 0, 0.95) !important; background: rgba(0, 0, 0, 0.95) !important;
backdrop-filter: blur(20px) !important; backdrop-filter: blur(20px) !important;
border-radius: 12px !important; border-radius: 12px !important;
padding: 8px !important; padding: 8px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important; border: 1px solid rgba(255, 255, 255, 0.1) !important;
} }
.lk-media-device-select button { .lk-media-device-select button {
border-radius: 8px !important; border-radius: 8px !important;
padding: 10px 14px !important; padding: 10px 14px !important;
transition: background 0.2s ease !important; transition: background 0.2s ease !important;
width: 100% !important; width: 100% !important;
min-width: 0 !important; min-width: 0 !important;
white-space: normal !important; white-space: normal !important;
text-align: left !important; text-align: left !important;
} }
.lk-media-device-select button:hover { .lk-media-device-select button:hover {
background: rgba(255, 255, 255, 0.1) !important; background: rgba(255, 255, 255, 0.1) !important;
} }
.lk-media-device-select button[data-lk-active="true"] { .lk-media-device-select button[data-lk-active="true"] {
background: var(--md-sys-color-primary) !important; background: var(--md-sys-color-primary) !important;
} }
/* Индикатор говорящего */ /* Индикатор говорящего */
.lk-participant-tile[data-lk-speaking="true"] { .lk-participant-tile[data-lk-speaking="true"] {
box-shadow: 0 0 0 3px var(--md-sys-color-primary) !important; box-shadow: 0 0 0 3px var(--md-sys-color-primary) !important;
} }
/* Layout для 1-на-1: собеседник на весь экран, своя камера в углу */ /* Layout для 1-на-1: собеседник на весь экран, своя камера в углу */
/* Карусель position:absolute выходит из flow — остаётся только основной контент. */ /* Карусель position:absolute выходит из flow — остаётся только основной контент. */
/* Сетка 5fr 1fr: единственный grid-ребёнок (основное видео) получает 5fr (расширяется). */ /* Сетка 5fr 1fr: единственный grid-ребёнок (основное видео) получает 5fr (расширяется). */
.lk-focus-layout { .lk-focus-layout {
position: relative !important; position: relative !important;
grid-template-columns: 5fr 1fr !important; grid-template-columns: 5fr 1fr !important;
} }
/* Основное видео (собеседник) на весь экран */ /* Основное видео (собеседник) на весь экран */
.lk-focus-layout .lk-focus-layout-wrapper { .lk-focus-layout .lk-focus-layout-wrapper {
width: 100% !important; width: 100% !important;
height: 100% !important; height: 100% !important;
} }
.lk-focus-layout .lk-focus-layout-wrapper .lk-participant-tile { .lk-focus-layout .lk-focus-layout-wrapper .lk-participant-tile {
width: 100% !important; width: 100% !important;
height: 100% !important; height: 100% !important;
border-radius: 0 !important; border-radius: 0 !important;
} }
/* Демонстрация экрана — на весь экран только в режиме фокуса (после клика на раскрытие) */ /* Демонстрация экрана — на весь экран только в режиме фокуса (после клика на раскрытие) */
/* Структура: .lk-focus-layout-wrapper > .lk-focus-layout > .lk-participant-tile */ /* Структура: .lk-focus-layout-wrapper > .lk-focus-layout > .lk-participant-tile */
.lk-focus-layout > .lk-participant-tile[data-lk-source="screen_share"] { .lk-focus-layout > .lk-participant-tile[data-lk-source="screen_share"] {
position: absolute !important; position: absolute !important;
width: 100% !important; width: 100% !important;
height: 100% !important; height: 100% !important;
top: 0 !important; top: 0 !important;
left: 0 !important; left: 0 !important;
border-radius: 0 !important; border-radius: 0 !important;
z-index: 50 !important; z-index: 50 !important;
} }
/* Карусель с локальным видео (своя камера) */ /* Карусель с локальным видео (своя камера) */
.lk-focus-layout .lk-carousel { .lk-focus-layout .lk-carousel {
position: absolute !important; position: absolute !important;
bottom: 80px !important; bottom: 80px !important;
right: 16px !important; right: 16px !important;
width: 280px !important; width: 280px !important;
height: auto !important; height: auto !important;
z-index: 100 !important; z-index: 100 !important;
pointer-events: auto !important; pointer-events: auto !important;
} }
.lk-focus-layout .lk-carousel .lk-participant-tile { .lk-focus-layout .lk-carousel .lk-participant-tile {
width: 280px !important; width: 280px !important;
height: 158px !important; height: 158px !important;
border-radius: 12px !important; border-radius: 12px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6) !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6) !important;
border: 2px solid rgba(255, 255, 255, 0.2) !important; border: 2px solid rgba(255, 255, 255, 0.2) !important;
} }
/* Скрыть стрелки карусели (они не нужны для 1 участника) */ /* Скрыть стрелки карусели (они не нужны для 1 участника) */
.lk-focus-layout .lk-carousel button[aria-label*="Previous"], .lk-focus-layout .lk-carousel button[aria-label*="Previous"],
.lk-focus-layout .lk-carousel button[aria-label*="Next"] { .lk-focus-layout .lk-carousel button[aria-label*="Next"] {
display: none !important; display: none !important;
} }
/* Если используется grid layout (фоллбэк) */ /* Если используется grid layout (фоллбэк) */
.lk-grid-layout { .lk-grid-layout {
position: relative !important; position: relative !important;
} }
/* Для 2 участников: первый на весь экран, второй в углу */ /* Для 2 участников: первый на весь экран, второй в углу */
.lk-grid-layout[data-lk-participants="2"] { .lk-grid-layout[data-lk-participants="2"] {
display: block !important; display: block !important;
position: relative !important; position: relative !important;
} }
.lk-grid-layout[data-lk-participants="2"] .lk-participant-tile:first-child { .lk-grid-layout[data-lk-participants="2"] .lk-participant-tile:first-child {
position: absolute !important; position: absolute !important;
top: 0 !important; top: 0 !important;
left: 0 !important; left: 0 !important;
width: 100% !important; width: 100% !important;
height: 100% !important; height: 100% !important;
border-radius: 0 !important; border-radius: 0 !important;
} }
.lk-grid-layout[data-lk-participants="2"] .lk-participant-tile:last-child { .lk-grid-layout[data-lk-participants="2"] .lk-participant-tile:last-child {
position: absolute !important; position: absolute !important;
bottom: 80px !important; bottom: 80px !important;
right: 16px !important; right: 16px !important;
width: 280px !important; width: 280px !important;
height: 158px !important; height: 158px !important;
border-radius: 12px !important; border-radius: 12px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6) !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6) !important;
border: 2px solid rgba(255, 255, 255, 0.2) !important; border: 2px solid rgba(255, 255, 255, 0.2) !important;
z-index: 100 !important; z-index: 100 !important;
} }
/* Адаптивность */ /* Адаптивность */
@media (max-width: 768px) { @media (max-width: 768px) {
.lk-control-bar { .lk-control-bar {
border-radius: 12px !important; border-radius: 12px !important;
padding: 8px 12px !important; padding: 8px 12px !important;
} }
.lk-control-bar .lk-button, .lk-control-bar .lk-button,
.lk-button { .lk-button {
min-width: 44px !important; min-width: 44px !important;
width: auto !important; width: auto !important;
height: 44px !important; height: 44px !important;
} }
/* Уменьшаем размер локального видео на мобильных */ /* Уменьшаем размер локального видео на мобильных */
.lk-focus-layout .lk-carousel, .lk-focus-layout .lk-carousel,
.lk-grid-layout[data-lk-participants="2"] .lk-participant-tile:last-child { .lk-grid-layout[data-lk-participants="2"] .lk-participant-tile:last-child {
width: 160px !important; width: 160px !important;
height: 90px !important; height: 90px !important;
bottom: 70px !important; bottom: 70px !important;
right: 12px !important; right: 12px !important;
} }
} }
/* Качество отображения видео в контейнере LiveKit */ /* Качество отображения видео в контейнере LiveKit */
.lk-participant-media-video { .lk-participant-media-video {
background: #000 !important; background: #000 !important;
} }
/* Демонстрация экрана: contain чтобы не обрезать, чёткое отображение */ /* Демонстрация экрана: contain чтобы не обрезать, чёткое отображение */
.lk-participant-media-video[data-lk-source="screen_share"] { .lk-participant-media-video[data-lk-source="screen_share"] {
object-fit: contain !important; object-fit: contain !important;
object-position: center !important; object-position: center !important;
image-rendering: -webkit-optimize-contrast; image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges; image-rendering: crisp-edges;
} }
/* Сетка: минимальная высота плиток для крупного видео */ /* Сетка: минимальная высота плиток для крупного видео */
.lk-grid-layout { .lk-grid-layout {
min-height: 0; min-height: 0;
} }
.lk-grid-layout .lk-participant-tile { .lk-grid-layout .lk-participant-tile {
min-height: 240px; min-height: 240px;
} }