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';
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
Подробнее и оплатить
|
Подробнее и оплатить
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue