moobile
Deploy to Production / deploy-production (push) Successful in 27s
Details
Deploy to Production / deploy-production (push) Successful in 27s
Details
This commit is contained in:
parent
0b5fb434db
commit
b4b99491ae
|
|
@ -1,64 +1,64 @@
|
|||
import { AuthRedirect } from '@/components/auth/AuthRedirect';
|
||||
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<AuthRedirect>
|
||||
<div
|
||||
data-no-nav
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr minmax(0, 520px)',
|
||||
}}
|
||||
>
|
||||
{/* Левая колонка — пустая, фон как у body */}
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
<img
|
||||
src="/logo/logo.svg"
|
||||
alt="Uchill Logo"
|
||||
style={{
|
||||
width: '240px',
|
||||
height: 'auto',
|
||||
opacity: 0.8
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Правая колонка — форма на белом фоне */}
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '24px 32px',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '32px', textAlign: 'center' }}>
|
||||
<img
|
||||
src="/logo/logo.svg"
|
||||
alt="Uchill Logo"
|
||||
style={{ width: '120px', height: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</AuthRedirect>
|
||||
);
|
||||
}
|
||||
import { AuthRedirect } from '@/components/auth/AuthRedirect';
|
||||
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<AuthRedirect>
|
||||
<div
|
||||
data-no-nav
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr minmax(0, 520px)',
|
||||
}}
|
||||
>
|
||||
{/* Левая колонка — пустая, фон как у body */}
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
<img
|
||||
src="/logo/logo.svg"
|
||||
alt="Uchill Logo"
|
||||
style={{
|
||||
width: '240px',
|
||||
height: 'auto',
|
||||
opacity: 0.8
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Правая колонка — форма на белом фоне */}
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '24px 32px',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '32px', textAlign: 'center' }}>
|
||||
<img
|
||||
src="/logo/logo.svg"
|
||||
alt="Uchill Logo"
|
||||
style={{ width: '120px', height: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</AuthRedirect>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,15 +167,14 @@ export default function ChatPage() {
|
|||
}, [normalizeChat, refreshNavBadges]);
|
||||
|
||||
return (
|
||||
<div className="ios26-dashboard" style={{ padding: '16px' }}>
|
||||
<div className="ios26-dashboard ios26-chat-page" style={{ padding: '16px' }}>
|
||||
<Box
|
||||
className="ios26-chat-layout"
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '320px 1fr',
|
||||
gap: 'var(--ios26-spacing)',
|
||||
alignItems: 'stretch',
|
||||
// учитываем padding контейнера (16px сверху и снизу),
|
||||
// чтобы итоговая высота блока была ~90vh
|
||||
height: 'calc(90vh - 32px)',
|
||||
maxHeight: 'calc(90vh - 32px)',
|
||||
overflow: 'hidden',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,19 @@
|
|||
'use client';
|
||||
|
||||
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 { BottomNavigationBar } from '@/components/navigation/BottomNavigationBar';
|
||||
import { TopNavigationBar } from '@/components/navigation/TopNavigationBar';
|
||||
|
|
@ -22,6 +35,7 @@ export default function ProtectedLayout({
|
|||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { user, loading } = useAuth();
|
||||
const isMobile = useIsMobile();
|
||||
const [navBadges, setNavBadges] = useState<NavBadges | null>(null);
|
||||
const [subscriptionChecked, setSubscriptionChecked] = useState(false);
|
||||
|
||||
|
|
@ -139,25 +153,45 @@ export default function ProtectedLayout({
|
|||
return (
|
||||
<NavBadgesProvider refreshNavBadges={refreshNavBadges}>
|
||||
<SelectedChildProvider>
|
||||
{!isFullWidthPage && <TopNavigationBar user={user} />}
|
||||
<main
|
||||
data-no-nav={isLiveKit ? true : undefined}
|
||||
<div
|
||||
className="protected-layout-root"
|
||||
style={{
|
||||
padding: isFullWidthPage ? '0' : '16px',
|
||||
maxWidth: isFullWidthPage ? '100%' : '1200px',
|
||||
margin: isFullWidthPage ? '0' : '0 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '100vh',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
{!isLiveKit && (
|
||||
<Suspense fallback={null}>
|
||||
<BottomNavigationBar userRole={user?.role} user={user} navBadges={navBadges} />
|
||||
</Suspense>
|
||||
)}
|
||||
{!isLiveKit && user && (
|
||||
<NotificationBell />
|
||||
)}
|
||||
{!isFullWidthPage && <TopNavigationBar user={user} />}
|
||||
<main
|
||||
className="protected-main"
|
||||
data-no-nav={isLiveKit ? true : undefined}
|
||||
data-full-width={isFullWidthPage ? true : undefined}
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
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>
|
||||
</NavBadgesProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -381,6 +381,7 @@ function ProfilePage() {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="page-profile"
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
padding: 24,
|
||||
|
|
@ -389,6 +390,7 @@ function ProfilePage() {
|
|||
}}
|
||||
>
|
||||
<div
|
||||
className="page-profile-grid"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(0, 440px) 1fr',
|
||||
|
|
@ -618,13 +620,7 @@ function ProfilePage() {
|
|||
|
||||
{/* Поля — 2 колонки */}
|
||||
<div style={{ padding: '0 24px 24px 24px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '12px 16px',
|
||||
}}
|
||||
>
|
||||
<div className="page-profile-fields" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 16px' }}>
|
||||
<div>
|
||||
<label htmlFor="profile-first-name" style={{ display: 'block', fontSize: 12, fontWeight: 500, color: '#858585', marginBottom: 4 }}>Имя</label>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -405,10 +405,10 @@ export default function SchedulePage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="ios26-dashboard" style={{ padding: '16px' }}>
|
||||
<div className="ios26-dashboard ios26-schedule-page" style={{ padding: '16px' }}>
|
||||
{error && <ErrorDisplay error={error} onRetry={loadLessons} />}
|
||||
|
||||
<div style={{
|
||||
<div className="ios26-schedule-layout" style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '5fr 2fr',
|
||||
gap: 'var(--ios26-spacing)',
|
||||
|
|
@ -417,44 +417,47 @@ export default function SchedulePage() {
|
|||
// чтобы календарь не “схлопывался” на месяцах с меньшим количеством контента
|
||||
minHeight: 'calc(100vh - 160px)',
|
||||
}}>
|
||||
<Calendar
|
||||
lessons={lessons}
|
||||
lessonsLoading={lessonsLoading}
|
||||
<div className="ios26-schedule-calendar-wrap">
|
||||
<Calendar
|
||||
lessons={lessons}
|
||||
lessonsLoading={lessonsLoading}
|
||||
selectedDate={selectedDate}
|
||||
onSelectSlot={handleSelectSlot}
|
||||
onSelectEvent={handleSelectEvent}
|
||||
onMonthChange={handleMonthChange}
|
||||
/>
|
||||
|
||||
<CheckLesson
|
||||
selectedDate={selectedDate}
|
||||
displayDate={displayDate}
|
||||
lessonsLoading={lessonsLoading}
|
||||
lessonsForSelectedDate={lessonsForSelectedDate}
|
||||
isFormVisible={isFormVisible}
|
||||
isMentor={isMentor}
|
||||
onPrevDay={handlePrevDay}
|
||||
onNextDay={handleNextDay}
|
||||
onAddLesson={handleAddLesson}
|
||||
onLessonClick={handleLessonClick}
|
||||
buttonComponentsLoaded={buttonComponentsLoaded}
|
||||
formComponentsLoaded={formComponentsLoaded}
|
||||
lessonEditLoading={lessonEditLoading}
|
||||
isEditingMode={isEditingMode}
|
||||
formLoading={formLoading}
|
||||
formError={formError}
|
||||
formData={formData}
|
||||
setFormData={setFormData}
|
||||
selectedSubjectId={selectedSubjectId}
|
||||
selectedMentorSubjectId={selectedMentorSubjectId}
|
||||
onSubjectChange={handleSubjectChange}
|
||||
students={students}
|
||||
subjects={subjects}
|
||||
mentorSubjects={mentorSubjects}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
onDelete={isEditingMode ? handleDelete : undefined}
|
||||
/>
|
||||
onSelectSlot={handleSelectSlot}
|
||||
onSelectEvent={handleSelectEvent}
|
||||
onMonthChange={handleMonthChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="ios26-schedule-right-wrap">
|
||||
<CheckLesson
|
||||
selectedDate={selectedDate}
|
||||
displayDate={displayDate}
|
||||
lessonsLoading={lessonsLoading}
|
||||
lessonsForSelectedDate={lessonsForSelectedDate}
|
||||
isFormVisible={isFormVisible}
|
||||
isMentor={isMentor}
|
||||
onPrevDay={handlePrevDay}
|
||||
onNextDay={handleNextDay}
|
||||
onAddLesson={handleAddLesson}
|
||||
onLessonClick={handleLessonClick}
|
||||
buttonComponentsLoaded={buttonComponentsLoaded}
|
||||
formComponentsLoaded={formComponentsLoaded}
|
||||
lessonEditLoading={lessonEditLoading}
|
||||
isEditingMode={isEditingMode}
|
||||
formLoading={formLoading}
|
||||
formError={formError}
|
||||
formData={formData}
|
||||
setFormData={setFormData}
|
||||
selectedSubjectId={selectedSubjectId}
|
||||
selectedMentorSubjectId={selectedMentorSubjectId}
|
||||
onSubjectChange={handleSubjectChange}
|
||||
students={students}
|
||||
subjects={subjects}
|
||||
mentorSubjects={mentorSubjects}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
onDelete={isEditingMode ? handleDelete : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -25,6 +25,8 @@ interface BottomNavigationBarProps {
|
|||
userRole?: string;
|
||||
user?: User | null;
|
||||
navBadges?: NavBadges | null;
|
||||
/** Слот для кнопки уведомлений (на мобильном — 4-й элемент в первом ряду). */
|
||||
notificationsSlot?: React.ReactNode;
|
||||
/** Выдвижная панель справа (3 колонки). При клике по пункту вызывается onClose. */
|
||||
slideout?: boolean;
|
||||
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 pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
|
@ -113,8 +115,8 @@ export function BottomNavigationBar({ userRole, user, navBadges, slideout, onClo
|
|||
return common;
|
||||
}, [userRole]);
|
||||
|
||||
const firstRowItems = navigationItems.slice(0, 5);
|
||||
const restItems = navigationItems.slice(5);
|
||||
const firstRowItems = navigationItems.slice(0, notificationsSlot ? 3 : 5);
|
||||
const restItems = navigationItems.slice(notificationsSlot ? 3 : 5);
|
||||
const hasMore = restItems.length > 0;
|
||||
|
||||
// Подсветка активного таба по текущему URL
|
||||
|
|
@ -270,22 +272,32 @@ export function BottomNavigationBar({ userRole, user, navBadges, slideout, onClo
|
|||
<div
|
||||
className={
|
||||
'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' ? (
|
||||
<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))}
|
||||
{notificationsSlot}
|
||||
</div>
|
||||
) : (
|
||||
firstRowItems.map((item, i) => renderButton(item, i))
|
||||
<>
|
||||
{firstRowItems.map((item, i) => renderButton(item, i))}
|
||||
{notificationsSlot}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ function NotificationItem({
|
|||
|
||||
const SCROLL_LOAD_MORE_THRESHOLD = 80;
|
||||
|
||||
export function NotificationBell() {
|
||||
export function NotificationBell({ embedded }: { embedded?: boolean }) {
|
||||
const refreshNavBadges = useNavBadgesRefresh();
|
||||
const {
|
||||
list,
|
||||
|
|
@ -164,16 +164,26 @@ export function NotificationBell() {
|
|||
|
||||
<div
|
||||
data-notification-bell
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: BELL_POSITION.right,
|
||||
bottom: BELL_POSITION.bottom,
|
||||
zIndex: 9998,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'flex-end',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
style={
|
||||
embedded
|
||||
? {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
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 && (
|
||||
|
|
@ -182,8 +192,9 @@ export function NotificationBell() {
|
|||
className="notification-panel-enter-active"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 52,
|
||||
bottom: 0,
|
||||
...(embedded
|
||||
? { bottom: '100%', marginBottom: 8, left: '50%', transform: 'translateX(-50%)' }
|
||||
: { right: 52, bottom: 0 }),
|
||||
width: PANEL_WIDTH,
|
||||
maxHeight: PANEL_MAX_HEIGHT,
|
||||
backgroundColor: 'var(--md-sys-color-surface)',
|
||||
|
|
@ -295,56 +306,97 @@ export function NotificationBell() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопка-колокольчик */}
|
||||
<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}
|
||||
{/* Кнопка-колокольчик: в меню — как пункт навигации, иначе — круглая */}
|
||||
{embedded ? (
|
||||
<button
|
||||
type="button"
|
||||
className="ios26-bottom-nav-button"
|
||||
aria-label={unreadCount > 0 ? `Уведомления: ${unreadCount} непрочитанных` : 'Уведомления'}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
>
|
||||
<span style={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<span className="material-symbols-outlined ios26-bottom-nav-icon">
|
||||
notifications
|
||||
</span>
|
||||
{unreadCount > 0 && (
|
||||
<span
|
||||
className="ios26-bottom-nav-badge"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
right: -16,
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
borderRadius: 9,
|
||||
background: 'var(--md-sys-color-error, #b3261e)',
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0 4px',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
title={`${unreadCount} непрочитанных`}
|
||||
>
|
||||
{unreadCount > 99 ? '99+' : unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<span className="ios26-bottom-nav-label">Уведомления</span>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -188,15 +188,21 @@ export function ProfilePaymentTab() {
|
|||
</ul>
|
||||
<div className="ios26-plan-card__actions">
|
||||
{free ? (
|
||||
<button
|
||||
type="button"
|
||||
className="ios26-plan-card__action"
|
||||
onClick={() => handleActivateFree(plan)}
|
||||
disabled={!!activatingPlanId}
|
||||
style={{ cursor: activatingPlanId ? 'wait' : 'pointer' }}
|
||||
>
|
||||
{activatingPlanId === plan.id ? 'Активация...' : 'Активировать'}
|
||||
</button>
|
||||
subscription ? (
|
||||
<span className="ios26-plan-card__action" style={{ opacity: 0.8, cursor: 'default' }}>
|
||||
Подписка уже активирована
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="ios26-plan-card__action"
|
||||
onClick={() => handleActivateFree(plan)}
|
||||
disabled={!!activatingPlanId}
|
||||
style={{ cursor: activatingPlanId ? 'wait' : 'pointer' }}
|
||||
>
|
||||
{activatingPlanId === plan.id ? 'Активация...' : 'Активировать'}
|
||||
</button>
|
||||
)
|
||||
) : (
|
||||
<Link href="/payment" className="ios26-plan-card__action">
|
||||
Подробнее и оплатить
|
||||
|
|
|
|||
|
|
@ -1,17 +1,26 @@
|
|||
'use client';
|
||||
|
||||
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 { useToast } from '@/contexts/ToastContext';
|
||||
|
||||
const formatCurrency = (v: number) =>
|
||||
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() {
|
||||
const { showToast } = useToast();
|
||||
const [profile, setProfile] = 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 [copied, setCopied] = useState(false);
|
||||
|
||||
|
|
@ -19,6 +28,7 @@ export function ReferralsPageContent() {
|
|||
Promise.all([
|
||||
getReferralProfile().then(setProfile),
|
||||
getReferralStats().then(setStats),
|
||||
getMyReferrals().then(setReferralsList).catch(() => setReferralsList({ direct: [], indirect: [] })),
|
||||
])
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
|
@ -138,6 +148,58 @@ export function ReferralsPageContent() {
|
|||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,6 +169,10 @@ body:has([data-no-nav]) {
|
|||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
body:has(.protected-layout-root) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
body > * {
|
||||
position: relative;
|
||||
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");
|
||||
}
|
||||
|
||||
/* Кастомный нижний бар iOS 26 — первый ряд всегда, остальное по раскрытию */
|
||||
/* Кастомный нижний бар iOS 26 — первый ряд всегда, остальное по раскрытию. Ноутбук и выше: fixed, bottom 20px */
|
||||
.ios26-bottom-nav-container {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
bottom: 20px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
z-index: 1000;
|
||||
|
|
@ -294,6 +298,70 @@ img {
|
|||
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 {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
|
|
@ -353,6 +421,15 @@ img {
|
|||
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 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
|
@ -1306,6 +1383,308 @@ img {
|
|||
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-card {
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -1,429 +1,429 @@
|
|||
/**
|
||||
* Кастомизация LiveKit через CSS переменные.
|
||||
* Все стили и скрипты LiveKit отдаются с нашего сервера (бандл + этот файл).
|
||||
*/
|
||||
|
||||
@keyframes lk-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Цвета фона */
|
||||
--lk-bg: #1a1a1a;
|
||||
--lk-bg2: #2a2a2a;
|
||||
--lk-bg3: #3a3a3a;
|
||||
|
||||
/* Цвета текста */
|
||||
--lk-fg: #ffffff;
|
||||
--lk-fg2: rgba(255, 255, 255, 0.7);
|
||||
|
||||
/* Основные цвета */
|
||||
--lk-control-bg: var(--md-sys-color-primary);
|
||||
--lk-control-hover-bg: var(--md-sys-color-primary-container);
|
||||
--lk-button-bg: rgba(255, 255, 255, 0.15);
|
||||
--lk-button-hover-bg: rgba(255, 255, 255, 0.25);
|
||||
|
||||
/* Границы */
|
||||
--lk-border-color: rgba(255, 255, 255, 0.1);
|
||||
--lk-border-radius: 12px;
|
||||
|
||||
/* Фокус */
|
||||
--lk-focus-ring: var(--md-sys-color-primary);
|
||||
|
||||
/* Ошибки */
|
||||
--lk-danger: var(--md-sys-color-error);
|
||||
|
||||
/* Размеры */
|
||||
--lk-control-bar-height: 80px;
|
||||
--lk-participant-tile-gap: 12px;
|
||||
}
|
||||
|
||||
/* Панель управления — без ограничения по ширине */
|
||||
.lk-control-bar {
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
backdrop-filter: blur(20px) !important;
|
||||
border-radius: 16px !important;
|
||||
padding: 12px 16px !important;
|
||||
margin: 16px !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important;
|
||||
max-width: none !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-button-group,
|
||||
.lk-control-bar .lk-button-group-menu {
|
||||
max-width: none !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
/* Кнопки управления — ширина по контенту, без жёсткого ограничения */
|
||||
.lk-control-bar .lk-button {
|
||||
min-width: 48px !important;
|
||||
width: auto !important;
|
||||
height: 48px !important;
|
||||
border-radius: 12px !important;
|
||||
transition: all 0.2s ease !important;
|
||||
padding-left: 12px !important;
|
||||
padding-right: 12px !important;
|
||||
}
|
||||
|
||||
/* Русские подписи: скрываем английский текст, показываем свой */
|
||||
.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="screen_share"],
|
||||
.lk-control-bar .lk-chat-toggle,
|
||||
.lk-control-bar .lk-disconnect-button,
|
||||
.lk-control-bar .lk-start-audio-button {
|
||||
font-size: 0 !important;
|
||||
}
|
||||
|
||||
.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="screen_share"] > svg,
|
||||
.lk-control-bar .lk-chat-toggle > svg,
|
||||
.lk-control-bar .lk-disconnect-button > svg {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-button[data-lk-source="microphone"]::after {
|
||||
content: "Микрофон";
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-button[data-lk-source="camera"]::after {
|
||||
content: "Камера";
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-button[data-lk-source="screen_share"]::after {
|
||||
content: "Поделиться экраном";
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-button[data-lk-source="screen_share"][data-lk-enabled="true"]::after {
|
||||
content: "Остановить демонстрацию";
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-chat-toggle::after {
|
||||
content: "Чат";
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Кнопка бургер слева от микрофона — в панели LiveKit */
|
||||
.lk-burger-button {
|
||||
background: rgba(255, 255, 255, 0.15) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* Скрываем стандартную кнопку «Выйти» — используем свою внутри панели (модалка: Выйти / Выйти и завершить занятие) */
|
||||
.lk-control-bar .lk-disconnect-button {
|
||||
display: none !important;
|
||||
}
|
||||
.lk-control-bar .lk-disconnect-button::after {
|
||||
content: "Выйти";
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Наша кнопка «Выйти» — внутри панели, рядом с «Поделиться экраном» */
|
||||
.lk-control-bar .lk-custom-exit-button {
|
||||
font-size: 0 !important;
|
||||
background: var(--md-sys-color-error) !important;
|
||||
color: #fff !important;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.lk-control-bar .lk-custom-exit-button::after {
|
||||
content: "Выйти";
|
||||
font-size: 1rem;
|
||||
}
|
||||
.lk-control-bar .lk-custom-exit-button > .material-symbols-outlined {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* Скрываем кнопку «Начать видео» — у нас свой StartAudioOverlay */
|
||||
.lk-control-bar .lk-start-audio-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Кнопки без текста (только иконка) — минимальный размер */
|
||||
.lk-button {
|
||||
min-width: 48px !important;
|
||||
width: auto !important;
|
||||
height: 48px !important;
|
||||
border-radius: 12px !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
.lk-button:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.lk-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Активная кнопка */
|
||||
.lk-button[data-lk-enabled="true"] {
|
||||
background: var(--md-sys-color-primary) !important;
|
||||
}
|
||||
|
||||
/* Кнопка отключения — белые иконка и текст */
|
||||
.lk-disconnect-button {
|
||||
background: var(--md-sys-color-error) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.lk-disconnect-button > svg {
|
||||
color: #fff !important;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Плитки участников */
|
||||
.lk-participant-tile {
|
||||
border-radius: 12px !important;
|
||||
overflow: hidden !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
/* Плейсхолдер без камеры: скрываем дефолтную SVG, показываем аватар из API */
|
||||
.lk-participant-tile .lk-participant-placeholder svg {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Контейнер для аватара — нужен для container queries */
|
||||
.lk-participant-tile .lk-participant-placeholder {
|
||||
container-type: size;
|
||||
}
|
||||
|
||||
.lk-participant-tile .lk-participant-placeholder img.lk-participant-avatar-img {
|
||||
/* Квадрат: меньшая сторона контейнера, максимум 400px */
|
||||
--avatar-size: min(min(80cqw, 80cqh), 400px);
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Fallback для браузеров без container queries */
|
||||
@supports not (width: 1cqw) {
|
||||
.lk-participant-tile .lk-participant-placeholder img.lk-participant-avatar-img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Имя участника — белый текст (Камера, PiP) */
|
||||
.lk-participant-name {
|
||||
background: rgba(0, 0, 0, 0.7) !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 6px 12px !important;
|
||||
font-weight: 600 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* Чат LiveKit скрыт — используем чат сервиса (платформы) */
|
||||
.lk-video-conference .lk-chat {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-chat-toggle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Стили чата платформы оставляем для других страниц */
|
||||
.lk-chat {
|
||||
background: var(--md-sys-color-surface) !important;
|
||||
border-left: 1px solid var(--md-sys-color-outline) !important;
|
||||
}
|
||||
|
||||
.lk-chat-entry {
|
||||
background: var(--md-sys-color-surface-container) !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 12px !important;
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
/* Сетка участников */
|
||||
.lk-grid-layout {
|
||||
gap: 12px !important;
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
/* Меню выбора устройств — без ограничения по ширине */
|
||||
.lk-device-menu,
|
||||
.lk-media-device-select {
|
||||
max-width: none !important;
|
||||
width: max-content !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.lk-media-device-select {
|
||||
background: rgba(0, 0, 0, 0.95) !important;
|
||||
backdrop-filter: blur(20px) !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 8px !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.lk-media-device-select button {
|
||||
border-radius: 8px !important;
|
||||
padding: 10px 14px !important;
|
||||
transition: background 0.2s ease !important;
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
white-space: normal !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.lk-media-device-select button:hover {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.lk-media-device-select button[data-lk-active="true"] {
|
||||
background: var(--md-sys-color-primary) !important;
|
||||
}
|
||||
|
||||
/* Индикатор говорящего */
|
||||
.lk-participant-tile[data-lk-speaking="true"] {
|
||||
box-shadow: 0 0 0 3px var(--md-sys-color-primary) !important;
|
||||
}
|
||||
|
||||
/* Layout для 1-на-1: собеседник на весь экран, своя камера в углу */
|
||||
/* Карусель position:absolute выходит из flow — остаётся только основной контент. */
|
||||
/* Сетка 5fr 1fr: единственный grid-ребёнок (основное видео) получает 5fr (расширяется). */
|
||||
.lk-focus-layout {
|
||||
position: relative !important;
|
||||
grid-template-columns: 5fr 1fr !important;
|
||||
}
|
||||
|
||||
/* Основное видео (собеседник) на весь экран */
|
||||
.lk-focus-layout .lk-focus-layout-wrapper {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.lk-focus-layout .lk-focus-layout-wrapper .lk-participant-tile {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Демонстрация экрана — на весь экран только в режиме фокуса (после клика на раскрытие) */
|
||||
/* Структура: .lk-focus-layout-wrapper > .lk-focus-layout > .lk-participant-tile */
|
||||
.lk-focus-layout > .lk-participant-tile[data-lk-source="screen_share"] {
|
||||
position: absolute !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
z-index: 50 !important;
|
||||
}
|
||||
|
||||
/* Карусель с локальным видео (своя камера) */
|
||||
.lk-focus-layout .lk-carousel {
|
||||
position: absolute !important;
|
||||
bottom: 80px !important;
|
||||
right: 16px !important;
|
||||
width: 280px !important;
|
||||
height: auto !important;
|
||||
z-index: 100 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.lk-focus-layout .lk-carousel .lk-participant-tile {
|
||||
width: 280px !important;
|
||||
height: 158px !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6) !important;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
/* Скрыть стрелки карусели (они не нужны для 1 участника) */
|
||||
.lk-focus-layout .lk-carousel button[aria-label*="Previous"],
|
||||
.lk-focus-layout .lk-carousel button[aria-label*="Next"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Если используется grid layout (фоллбэк) */
|
||||
.lk-grid-layout {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* Для 2 участников: первый на весь экран, второй в углу */
|
||||
.lk-grid-layout[data-lk-participants="2"] {
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.lk-grid-layout[data-lk-participants="2"] .lk-participant-tile:first-child {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.lk-grid-layout[data-lk-participants="2"] .lk-participant-tile:last-child {
|
||||
position: absolute !important;
|
||||
bottom: 80px !important;
|
||||
right: 16px !important;
|
||||
width: 280px !important;
|
||||
height: 158px !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6) !important;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.lk-control-bar {
|
||||
border-radius: 12px !important;
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-button,
|
||||
.lk-button {
|
||||
min-width: 44px !important;
|
||||
width: auto !important;
|
||||
height: 44px !important;
|
||||
}
|
||||
|
||||
/* Уменьшаем размер локального видео на мобильных */
|
||||
.lk-focus-layout .lk-carousel,
|
||||
.lk-grid-layout[data-lk-participants="2"] .lk-participant-tile:last-child {
|
||||
width: 160px !important;
|
||||
height: 90px !important;
|
||||
bottom: 70px !important;
|
||||
right: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Качество отображения видео в контейнере LiveKit */
|
||||
.lk-participant-media-video {
|
||||
background: #000 !important;
|
||||
}
|
||||
/* Демонстрация экрана: contain чтобы не обрезать, чёткое отображение */
|
||||
.lk-participant-media-video[data-lk-source="screen_share"] {
|
||||
object-fit: contain !important;
|
||||
object-position: center !important;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
/* Сетка: минимальная высота плиток для крупного видео */
|
||||
.lk-grid-layout {
|
||||
min-height: 0;
|
||||
}
|
||||
.lk-grid-layout .lk-participant-tile {
|
||||
min-height: 240px;
|
||||
}
|
||||
/**
|
||||
* Кастомизация LiveKit через CSS переменные.
|
||||
* Все стили и скрипты LiveKit отдаются с нашего сервера (бандл + этот файл).
|
||||
*/
|
||||
|
||||
@keyframes lk-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Цвета фона */
|
||||
--lk-bg: #1a1a1a;
|
||||
--lk-bg2: #2a2a2a;
|
||||
--lk-bg3: #3a3a3a;
|
||||
|
||||
/* Цвета текста */
|
||||
--lk-fg: #ffffff;
|
||||
--lk-fg2: rgba(255, 255, 255, 0.7);
|
||||
|
||||
/* Основные цвета */
|
||||
--lk-control-bg: var(--md-sys-color-primary);
|
||||
--lk-control-hover-bg: var(--md-sys-color-primary-container);
|
||||
--lk-button-bg: rgba(255, 255, 255, 0.15);
|
||||
--lk-button-hover-bg: rgba(255, 255, 255, 0.25);
|
||||
|
||||
/* Границы */
|
||||
--lk-border-color: rgba(255, 255, 255, 0.1);
|
||||
--lk-border-radius: 12px;
|
||||
|
||||
/* Фокус */
|
||||
--lk-focus-ring: var(--md-sys-color-primary);
|
||||
|
||||
/* Ошибки */
|
||||
--lk-danger: var(--md-sys-color-error);
|
||||
|
||||
/* Размеры */
|
||||
--lk-control-bar-height: 80px;
|
||||
--lk-participant-tile-gap: 12px;
|
||||
}
|
||||
|
||||
/* Панель управления — без ограничения по ширине */
|
||||
.lk-control-bar {
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
backdrop-filter: blur(20px) !important;
|
||||
border-radius: 16px !important;
|
||||
padding: 12px 16px !important;
|
||||
margin: 16px !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important;
|
||||
max-width: none !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-button-group,
|
||||
.lk-control-bar .lk-button-group-menu {
|
||||
max-width: none !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
/* Кнопки управления — ширина по контенту, без жёсткого ограничения */
|
||||
.lk-control-bar .lk-button {
|
||||
min-width: 48px !important;
|
||||
width: auto !important;
|
||||
height: 48px !important;
|
||||
border-radius: 12px !important;
|
||||
transition: all 0.2s ease !important;
|
||||
padding-left: 12px !important;
|
||||
padding-right: 12px !important;
|
||||
}
|
||||
|
||||
/* Русские подписи: скрываем английский текст, показываем свой */
|
||||
.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="screen_share"],
|
||||
.lk-control-bar .lk-chat-toggle,
|
||||
.lk-control-bar .lk-disconnect-button,
|
||||
.lk-control-bar .lk-start-audio-button {
|
||||
font-size: 0 !important;
|
||||
}
|
||||
|
||||
.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="screen_share"] > svg,
|
||||
.lk-control-bar .lk-chat-toggle > svg,
|
||||
.lk-control-bar .lk-disconnect-button > svg {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-button[data-lk-source="microphone"]::after {
|
||||
content: "Микрофон";
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-button[data-lk-source="camera"]::after {
|
||||
content: "Камера";
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-button[data-lk-source="screen_share"]::after {
|
||||
content: "Поделиться экраном";
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-button[data-lk-source="screen_share"][data-lk-enabled="true"]::after {
|
||||
content: "Остановить демонстрацию";
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-chat-toggle::after {
|
||||
content: "Чат";
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Кнопка бургер слева от микрофона — в панели LiveKit */
|
||||
.lk-burger-button {
|
||||
background: rgba(255, 255, 255, 0.15) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* Скрываем стандартную кнопку «Выйти» — используем свою внутри панели (модалка: Выйти / Выйти и завершить занятие) */
|
||||
.lk-control-bar .lk-disconnect-button {
|
||||
display: none !important;
|
||||
}
|
||||
.lk-control-bar .lk-disconnect-button::after {
|
||||
content: "Выйти";
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Наша кнопка «Выйти» — внутри панели, рядом с «Поделиться экраном» */
|
||||
.lk-control-bar .lk-custom-exit-button {
|
||||
font-size: 0 !important;
|
||||
background: var(--md-sys-color-error) !important;
|
||||
color: #fff !important;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.lk-control-bar .lk-custom-exit-button::after {
|
||||
content: "Выйти";
|
||||
font-size: 1rem;
|
||||
}
|
||||
.lk-control-bar .lk-custom-exit-button > .material-symbols-outlined {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* Скрываем кнопку «Начать видео» — у нас свой StartAudioOverlay */
|
||||
.lk-control-bar .lk-start-audio-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Кнопки без текста (только иконка) — минимальный размер */
|
||||
.lk-button {
|
||||
min-width: 48px !important;
|
||||
width: auto !important;
|
||||
height: 48px !important;
|
||||
border-radius: 12px !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
.lk-button:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.lk-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Активная кнопка */
|
||||
.lk-button[data-lk-enabled="true"] {
|
||||
background: var(--md-sys-color-primary) !important;
|
||||
}
|
||||
|
||||
/* Кнопка отключения — белые иконка и текст */
|
||||
.lk-disconnect-button {
|
||||
background: var(--md-sys-color-error) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.lk-disconnect-button > svg {
|
||||
color: #fff !important;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Плитки участников */
|
||||
.lk-participant-tile {
|
||||
border-radius: 12px !important;
|
||||
overflow: hidden !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
/* Плейсхолдер без камеры: скрываем дефолтную SVG, показываем аватар из API */
|
||||
.lk-participant-tile .lk-participant-placeholder svg {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Контейнер для аватара — нужен для container queries */
|
||||
.lk-participant-tile .lk-participant-placeholder {
|
||||
container-type: size;
|
||||
}
|
||||
|
||||
.lk-participant-tile .lk-participant-placeholder img.lk-participant-avatar-img {
|
||||
/* Квадрат: меньшая сторона контейнера, максимум 400px */
|
||||
--avatar-size: min(min(80cqw, 80cqh), 400px);
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Fallback для браузеров без container queries */
|
||||
@supports not (width: 1cqw) {
|
||||
.lk-participant-tile .lk-participant-placeholder img.lk-participant-avatar-img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Имя участника — белый текст (Камера, PiP) */
|
||||
.lk-participant-name {
|
||||
background: rgba(0, 0, 0, 0.7) !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 6px 12px !important;
|
||||
font-weight: 600 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* Чат LiveKit скрыт — используем чат сервиса (платформы) */
|
||||
.lk-video-conference .lk-chat {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-chat-toggle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Стили чата платформы оставляем для других страниц */
|
||||
.lk-chat {
|
||||
background: var(--md-sys-color-surface) !important;
|
||||
border-left: 1px solid var(--md-sys-color-outline) !important;
|
||||
}
|
||||
|
||||
.lk-chat-entry {
|
||||
background: var(--md-sys-color-surface-container) !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 12px !important;
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
/* Сетка участников */
|
||||
.lk-grid-layout {
|
||||
gap: 12px !important;
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
/* Меню выбора устройств — без ограничения по ширине */
|
||||
.lk-device-menu,
|
||||
.lk-media-device-select {
|
||||
max-width: none !important;
|
||||
width: max-content !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.lk-media-device-select {
|
||||
background: rgba(0, 0, 0, 0.95) !important;
|
||||
backdrop-filter: blur(20px) !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 8px !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.lk-media-device-select button {
|
||||
border-radius: 8px !important;
|
||||
padding: 10px 14px !important;
|
||||
transition: background 0.2s ease !important;
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
white-space: normal !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.lk-media-device-select button:hover {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.lk-media-device-select button[data-lk-active="true"] {
|
||||
background: var(--md-sys-color-primary) !important;
|
||||
}
|
||||
|
||||
/* Индикатор говорящего */
|
||||
.lk-participant-tile[data-lk-speaking="true"] {
|
||||
box-shadow: 0 0 0 3px var(--md-sys-color-primary) !important;
|
||||
}
|
||||
|
||||
/* Layout для 1-на-1: собеседник на весь экран, своя камера в углу */
|
||||
/* Карусель position:absolute выходит из flow — остаётся только основной контент. */
|
||||
/* Сетка 5fr 1fr: единственный grid-ребёнок (основное видео) получает 5fr (расширяется). */
|
||||
.lk-focus-layout {
|
||||
position: relative !important;
|
||||
grid-template-columns: 5fr 1fr !important;
|
||||
}
|
||||
|
||||
/* Основное видео (собеседник) на весь экран */
|
||||
.lk-focus-layout .lk-focus-layout-wrapper {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.lk-focus-layout .lk-focus-layout-wrapper .lk-participant-tile {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Демонстрация экрана — на весь экран только в режиме фокуса (после клика на раскрытие) */
|
||||
/* Структура: .lk-focus-layout-wrapper > .lk-focus-layout > .lk-participant-tile */
|
||||
.lk-focus-layout > .lk-participant-tile[data-lk-source="screen_share"] {
|
||||
position: absolute !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
z-index: 50 !important;
|
||||
}
|
||||
|
||||
/* Карусель с локальным видео (своя камера) */
|
||||
.lk-focus-layout .lk-carousel {
|
||||
position: absolute !important;
|
||||
bottom: 80px !important;
|
||||
right: 16px !important;
|
||||
width: 280px !important;
|
||||
height: auto !important;
|
||||
z-index: 100 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.lk-focus-layout .lk-carousel .lk-participant-tile {
|
||||
width: 280px !important;
|
||||
height: 158px !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6) !important;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
/* Скрыть стрелки карусели (они не нужны для 1 участника) */
|
||||
.lk-focus-layout .lk-carousel button[aria-label*="Previous"],
|
||||
.lk-focus-layout .lk-carousel button[aria-label*="Next"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Если используется grid layout (фоллбэк) */
|
||||
.lk-grid-layout {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* Для 2 участников: первый на весь экран, второй в углу */
|
||||
.lk-grid-layout[data-lk-participants="2"] {
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.lk-grid-layout[data-lk-participants="2"] .lk-participant-tile:first-child {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.lk-grid-layout[data-lk-participants="2"] .lk-participant-tile:last-child {
|
||||
position: absolute !important;
|
||||
bottom: 80px !important;
|
||||
right: 16px !important;
|
||||
width: 280px !important;
|
||||
height: 158px !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6) !important;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.lk-control-bar {
|
||||
border-radius: 12px !important;
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
|
||||
.lk-control-bar .lk-button,
|
||||
.lk-button {
|
||||
min-width: 44px !important;
|
||||
width: auto !important;
|
||||
height: 44px !important;
|
||||
}
|
||||
|
||||
/* Уменьшаем размер локального видео на мобильных */
|
||||
.lk-focus-layout .lk-carousel,
|
||||
.lk-grid-layout[data-lk-participants="2"] .lk-participant-tile:last-child {
|
||||
width: 160px !important;
|
||||
height: 90px !important;
|
||||
bottom: 70px !important;
|
||||
right: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Качество отображения видео в контейнере LiveKit */
|
||||
.lk-participant-media-video {
|
||||
background: #000 !important;
|
||||
}
|
||||
/* Демонстрация экрана: contain чтобы не обрезать, чёткое отображение */
|
||||
.lk-participant-media-video[data-lk-source="screen_share"] {
|
||||
object-fit: contain !important;
|
||||
object-position: center !important;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
/* Сетка: минимальная высота плиток для крупного видео */
|
||||
.lk-grid-layout {
|
||||
min-height: 0;
|
||||
}
|
||||
.lk-grid-layout .lk-participant-tile {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue