From 822a72e246ade671ae217de5c0c9ade4b04811a9a75d6286d18729deb8193497 Mon Sep 17 00:00:00 2001 From: peradas Date: Wed, 2 Sep 2026 11:43:47 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shine-UI/js/pages/user-profile-view.js | 372 +++++++++++-- shine-UI/styles/components.css | 694 ++++++++++++++++++++++++- 2 files changed, 1021 insertions(+), 45 deletions(-) diff --git a/shine-UI/js/pages/user-profile-view.js b/shine-UI/js/pages/user-profile-view.js index 884d6ab2..c3bf2b2c 100644 --- a/shine-UI/js/pages/user-profile-view.js +++ b/shine-UI/js/pages/user-profile-view.js @@ -1,56 +1,342 @@ import { renderHeader } from '../components/header.js'; -import { state } from '../state.js'; -import { loadUserProfileCard } from '../services/user-connections.js'; +import { SHINE_CONNECTIONS_LOGO_SRC } from '../components/shine-logo.js'; import { renderUserAvatar } from '../components/avatar-image.js'; +import { authService, state } from '../state.js'; +import { loadRelationsForPair, loadUserProfileCard } from '../services/user-connections.js'; import { makeProfileLinksRoute } from '../services/shine-routes.js'; import { navigateBack } from '../router.js'; -export const pageMeta = { id: 'user', title: 'Профиль пользователя' }; +export const pageMeta = { id: 'user', title: 'Профиль' }; -function escapeHtml(text){return String(text||'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"').replaceAll("'",''');} -function fullName(card){return [card.firstName,card.lastName].filter(Boolean).join(' ')||card.login;} - -function openTextModal(title,text){ - const root=document.getElementById('modal-root'); if(!root)return; - root.innerHTML=``; - const close=()=>{root.innerHTML='';}; root.querySelector('#profile-text-close')?.addEventListener('click',close); root.querySelector('#profile-text-modal')?.addEventListener('click',e=>{if(e.target?.id==='profile-text-modal')close();}); +function escapeHtml(text) { + return String(text || '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); } -function statusBadges(card){ - const role=card.accountRole==='primary'?'Основной аккаунт':card.accountRole==='non_voting'?'Голос не учитывать':''; - const shine=card.shineStatus==='shining'?'Сияющий':''; - return `
${role?`${escapeHtml(role)}`:''}${shine?'Сияющий':''}${card.shineStatus==='not_interested'?'Сияние неинтересно':''}
`; +function effectiveSocial(flags = {}) { + if (flags.outCloseFriend) return 'close_friend'; + if (flags.outFriend) return 'friend'; + if (flags.outContact) return 'contact'; + return 'none'; } -function statsHtml(card){ - const s=card.stats||{}; - const rows=[ - ['friends','Друзья',s.friendsCount],['close_friends','Близкие друзья',s.closeFriendsCount], - ['primary_received','Подтвердили основной аккаунт',s.primaryReceivedCount],['primary_given','Подтверждённые аккаунты',s.primaryGivenCount], - ['shine_received','Считают сияющим',s.shineReceivedCount],['shine_given','Подтверждённые сияющие',s.shineGivenCount], - ['channels_following','Подписки на каналы',s.followingChannelsCount],['channels_owned','Каналы',s.ownedPublicChannelsCount], - ]; - return `
${rows.map(([kind,label,n])=>``).join('')}
`; +function metricHtml({ kind, label, value, glow = false, positionClass = '' }) { + const numericValue = Number(value || 0); + return ` + `; } -export function render({navigate,route}){ - const requestedLogin=String(route?.params?.login||'').trim(); const selfLogin=String(state.session.login||'').trim(); - const screen=document.createElement('section'); screen.className='stack'; const body=document.createElement('div');body.className='stack';const status=document.createElement('div');status.className='status-line';status.textContent='Загрузка профиля...'; - screen.append(renderHeader({title:'Профиль пользователя',leftAction:{label:'←',onClick:()=>navigateBack()}}),status,body); - let card=null; - async function refresh(){ - card=await loadUserProfileCard(requestedLogin); const isSelf=card.login.toLowerCase()===selfLogin.toLowerCase(); - body.innerHTML=`${statusBadges(card)}
${escapeHtml(card.about||'')}
${statsHtml(card)} -
- ${!isSelf?`
`:''}`; - const identity=document.createElement('div');identity.className='card row';identity.style.gap='12px';identity.style.alignItems='center'; - identity.append(renderUserAvatar({login:card.login,firstName:card.firstName,lastName:card.lastName,avatar:card.avatar,size:'xl',className:'profile-avatar'})); - const txt=document.createElement('div');txt.innerHTML=`
${escapeHtml(fullName(card))}
${escapeHtml(card.login)}
`;identity.append(txt);body.prepend(identity);status.textContent=''; - } - body.addEventListener('click',e=>{const el=e.target.closest('[data-profile-list],[data-profile-detail],[data-bottom]');if(!el||!card)return; - const list=el.dataset.profileList;if(list){navigate(`SHiNE/${encodeURIComponent(card.login)}/list/${encodeURIComponent(list)}`);return;} - const detail=el.dataset.profileDetail;if(detail==='contacts'){openTextModal('Контакты',[card.web?`Links: ${card.web}`:'',card.phone?`Телефон: ${card.phone}`:'',card.address?`Адрес: ${card.address}`:''].filter(Boolean).join('\n')||'Не заполнено');return;} if(detail==='spiritual'){openTextModal('Духовный путь',card.spiritualPath);return;} - const bottom=el.dataset.bottom;if(bottom==='chat')navigate(`chat/${encodeURIComponent(card.login)}`);if(bottom==='links')navigate(makeProfileLinksRoute(card.login));if(bottom==='add')navigate(`SHiNE/${encodeURIComponent(card.login)}/manage`); - }); - refresh().catch(e=>{status.className='status-line is-unavailable';status.textContent=`Ошибка: ${e.message||'unknown'}`;}); return screen; +function openProfileSheet(title, html) { + const root = document.getElementById('modal-root'); + if (!root) return; + root.innerHTML = ` +
+ +
`; + + const close = () => { + if (root.querySelector('#user-profile-sheet-backdrop')) root.innerHTML = ''; + }; + root.querySelector('#user-profile-sheet-backdrop')?.addEventListener('click', (event) => { + if (event.target?.id === 'user-profile-sheet-backdrop') close(); + }); +} + +function aboutSheetHtml(card) { + return `
${escapeHtml(card?.about || 'Не заполнено')}
`; +} + +function contactsSheetHtml(card) { + const rows = [ + ['Ссылки', card?.web], + ['Телефон', card?.phone], + ['Адрес', card?.address], + ].filter(([, value]) => String(value || '').trim()); + + if (!rows.length) { + return '
Не заполнено
'; + } + return rows.map(([label, value]) => ` +
+ ${escapeHtml(label)} + ${escapeHtml(value)} +
`).join(''); +} + +function addIconHtml() { + return ` + `; +} + +function relationMenuHtml(flags = {}) { + const current = effectiveSocial(flags); + const rows = [ + ['friend', 'Друг'], + ['close_friend', 'Близкий друг'], + ['contact', 'Контакт'], + ]; + return rows.map(([kind, label]) => ` + `).join(''); +} + +export function render({ navigate, route, chrome }) { + const requestedLogin = String(route?.params?.login || '').trim(); + const selfLogin = String(state.session.login || '').trim(); + const screen = document.createElement('section'); + screen.className = 'stack user-profile-screen'; + + const header = renderHeader({ + title: requestedLogin || 'Профиль', + leftAction: { label: '←', onClick: () => navigateBack() }, + }); + header.classList.add('user-profile-header'); + chrome?.setTopbar(header); + + const status = document.createElement('div'); + status.className = 'status-line user-profile-status'; + status.textContent = 'Загрузка профиля...'; + + const body = document.createElement('div'); + body.className = 'user-profile-body'; + screen.append(status, body); + + let card = null; + let relationFlags = null; + let relationLoadPromise = null; + let addMenu = null; + let addActionButton = null; + + function updateRelationUi() { + if (!addMenu) return; + addMenu.innerHTML = relationMenuHtml(relationFlags || {}); + const social = effectiveSocial(relationFlags || {}); + addActionButton?.classList.toggle('is-active', social !== 'none'); + addActionButton?.setAttribute('aria-label', social === 'none' ? 'Добавить' : 'Изменить связь'); + } + + async function ensureRelationFlags() { + if (relationFlags) return relationFlags; + if (relationLoadPromise) return relationLoadPromise; + if (!selfLogin || !card?.login) return {}; + relationLoadPromise = loadRelationsForPair({ currentLogin: selfLogin, targetLogin: card.login }) + .then((flags) => { + relationFlags = flags || {}; + updateRelationUi(); + return relationFlags; + }) + .finally(() => { + relationLoadPromise = null; + }); + return relationLoadPromise; + } + + async function setRelationKind(kind, enabled) { + await authService.setUserRelation({ + login: selfLogin, + toLogin: card.login, + kind, + enabled, + storagePwd: state.session.storagePwdInMemory, + }); + } + + async function changeSocial(next) { + const flags = await ensureRelationFlags(); + const current = effectiveSocial(flags); + if (current === next) return; + + if (next === 'contact') { + if (flags.outCloseFriend) await setRelationKind('close_friend', false); + if (flags.outFriend) await setRelationKind('friend', false); + if (!flags.outContact) await setRelationKind('contact', true); + } + if (next === 'friend') { + if (flags.outCloseFriend) await setRelationKind('close_friend', false); + if (!flags.outFriend) await setRelationKind('friend', true); + } + if (next === 'close_friend' && !flags.outCloseFriend) { + await setRelationKind('close_friend', true); + } + + relationFlags = await loadRelationsForPair({ currentLogin: selfLogin, targetLogin: card.login }); + updateRelationUi(); + } + + function renderProfile() { + if (!card) return; + const isSelf = card.login.toLowerCase() === selfLogin.toLowerCase(); + const stats = card.stats || {}; + const official = card.accountRole === 'primary'; + const shining = card.shineStatus === 'shining'; + + const title = header.querySelector('.page-title'); + if (title) title.textContent = card.login; + + body.innerHTML = ` +
+ ${metricHtml({ kind: 'primary_received', label: 'Официальный', value: stats.primaryReceivedCount, glow: official, positionClass: 'is-top-left' })} + ${metricHtml({ kind: 'shine_received', label: 'Сияющий', value: stats.shineReceivedCount, glow: shining, positionClass: 'is-top-right' })} + + ${metricHtml({ kind: 'friends', label: 'Друзья', value: stats.friendsCount, positionClass: 'is-bottom-left' })} + ${metricHtml({ kind: 'close_friends', label: 'Близкие друзья', value: stats.closeFriendsCount, positionClass: 'is-bottom-right' })} +
+ +
+ ${metricHtml({ kind: 'channels_following', label: 'Подписки', value: stats.followingChannelsCount, positionClass: 'is-channel-metric' })} + ${metricHtml({ kind: 'channels_owned', label: 'Каналы', value: stats.ownedPublicChannelsCount, positionClass: 'is-channel-metric' })} +
+ + ${!isSelf ? ` +
+ + +
` : ''} + + `; + + const avatarSlot = body.querySelector('.user-profile-avatar-slot'); + avatarSlot?.append(renderUserAvatar({ + login: card.login, + firstName: card.firstName, + lastName: card.lastName, + avatar: card.avatar, + size: 'xl', + className: 'user-profile-hero-avatar', + glow: shining, + })); + + addMenu = body.querySelector('.user-profile-add-menu'); + addActionButton = body.querySelector('[data-profile-action="add"]'); + updateRelationUi(); + status.textContent = ''; + + if (!isSelf && selfLogin) { + void ensureRelationFlags().catch(() => {}); + } + } + + body.addEventListener('click', async (event) => { + if (!card) return; + const listButton = event.target.closest('[data-profile-list]'); + if (listButton) { + const kind = listButton.dataset.profileList; + if (kind) navigate(`SHiNE/${encodeURIComponent(card.login)}/list/${encodeURIComponent(kind)}`); + return; + } + + const detailButton = event.target.closest('[data-profile-detail]'); + if (detailButton?.dataset.profileDetail === 'about') { + openProfileSheet('О себе', aboutSheetHtml(card)); + return; + } + if (detailButton?.dataset.profileDetail === 'contacts') { + openProfileSheet('Контакты', contactsSheetHtml(card)); + return; + } + + const relationButton = event.target.closest('[data-relation-kind]'); + if (relationButton) { + if (!selfLogin) { + status.className = 'status-line user-profile-status is-unavailable'; + status.textContent = 'Для добавления пользователя необходимо войти.'; + return; + } + const next = relationButton.dataset.relationKind; + try { + addMenu?.classList.add('is-busy'); + await changeSocial(next); + if (addMenu) addMenu.hidden = true; + addActionButton?.setAttribute('aria-expanded', 'false'); + status.className = 'status-line user-profile-status'; + status.textContent = ''; + } catch (error) { + status.className = 'status-line user-profile-status is-unavailable'; + status.textContent = `Ошибка: ${error?.message || 'Не удалось изменить связь'}`; + } finally { + addMenu?.classList.remove('is-busy'); + } + return; + } + + const actionButton = event.target.closest('[data-profile-action]'); + const action = actionButton?.dataset.profileAction; + if (action === 'add') { + if (!addMenu) return; + const willOpen = addMenu.hidden; + addMenu.hidden = !willOpen; + actionButton.setAttribute('aria-expanded', willOpen ? 'true' : 'false'); + if (willOpen && selfLogin) void ensureRelationFlags().catch(() => {}); + return; + } + if (action === 'links') { + navigate(makeProfileLinksRoute(card.login)); + return; + } + if (action === 'chat') { + navigate(`chat/${encodeURIComponent(card.login)}`); + } + }); + + const handleOutsidePointer = (event) => { + if (!addMenu || addMenu.hidden) return; + if (event.target instanceof Node && body.contains(event.target)) { + const insideActions = event.target.closest?.('.user-profile-actions-wrap'); + if (insideActions) return; + } + addMenu.hidden = true; + addActionButton?.setAttribute('aria-expanded', 'false'); + }; + document.addEventListener('pointerdown', handleOutsidePointer); + + async function refresh() { + card = await loadUserProfileCard(requestedLogin); + renderProfile(); + } + + refresh().catch((error) => { + status.className = 'status-line user-profile-status is-unavailable'; + status.textContent = `Ошибка: ${error?.message || 'unknown'}`; + }); + + screen.cleanup = () => { + document.removeEventListener('pointerdown', handleOutsidePointer); + const root = document.getElementById('modal-root'); + if (root?.querySelector('#user-profile-sheet-backdrop')) root.innerHTML = ''; + }; + + return screen; } diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index bd2dbf05..4e4d3f7c 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -1847,7 +1847,7 @@ } .bubble-meta { - margin-top: 4px; + margin-top: 0; display: flex; justify-content: flex-end; align-items: center; @@ -2241,7 +2241,7 @@ } .call-peer-avatar-slot .call-peer-avatar .avatar-fallback { - font-size: clamp(34px, 9vw, 58px); + font-size: clamp(28px, 7.2vw, 46px); font-weight: 800; } @@ -10688,3 +10688,693 @@ body.chat-topbar-overlay .composer-slot { .chat-header-login-btn{display:flex;flex-direction:column;align-items:flex-start;line-height:1.1} .chat-header-display-name{font-size:14px;font-weight:650} .chat-header-user-login{font-size:11px;font-weight:400;opacity:.68;margin-top:2px} + +/* ===== Профиль другого пользователя: плавающая композиция без рамок (2026-09-01) ===== */ +.user-profile-screen { + --user-profile-avatar-size: min(33.6vw, 134px); + --user-profile-metric-size: clamp(42px, 12vw, 48px); + position: relative; + min-height: 100%; + gap: 0; + isolation: isolate; + overflow-x: hidden; + margin-top: -8px; +} + +.user-profile-screen::before { + content: ''; + position: absolute; + z-index: -1; + inset: 4% 0 14%; + pointer-events: none; + background: + radial-gradient(circle at 50% 27%, rgba(73, 113, 189, 0.16), transparent 29%), + radial-gradient(circle at 25% 58%, rgba(49, 198, 255, 0.08), transparent 24%), + radial-gradient(circle at 76% 56%, rgba(224, 190, 86, 0.07), transparent 25%); + filter: blur(18px); +} + + +.user-profile-status { + min-height: 0; + margin: 0; + text-align: center; + font-size: 12px; +} + +.user-profile-status:empty { + display: none; +} + +.user-profile-body { + display: flex; + flex-direction: column; + align-items: stretch; + min-height: 0; + padding: 0 0 10px; +} + +.user-profile-hero { + display: grid; + grid-template-columns: minmax(0, 1fr) var(--user-profile-avatar-size) minmax(0, 1fr); + grid-template-rows: 1fr 1fr; + align-items: stretch; + width: 100%; + height: var(--user-profile-avatar-size); + margin-top: 0; +} + +.user-profile-avatar-slot { + grid-column: 2; + grid-row: 1 / 3; + width: var(--user-profile-avatar-size); + height: var(--user-profile-avatar-size); + display: grid; + place-items: center; + align-self: center; + justify-self: center; + z-index: 2; +} + +.user-profile-screen .user-profile-hero-avatar.avatar, +.user-profile-screen .user-profile-hero-avatar.avatar-image { + width: 100%; + height: 100%; + min-width: 100%; + min-height: 100%; + overflow: visible; + border: 0 !important; + background: transparent; + box-shadow: none; +} + +/* На профиле используем ту же стеклянную обводку аватара, что и в остальных местах приложения. */ +.user-profile-screen .user-profile-hero-avatar.avatar-framed::after { + display: block; +} + +.user-profile-screen .user-profile-hero-avatar.avatar-framed > .avatar-fallback, +.user-profile-screen .user-profile-hero-avatar.avatar-framed > .avatar-photo { + width: 92.5%; + height: 92.5%; +} + +.user-profile-screen .user-profile-hero-avatar.avatar-glow::before { + inset: -12%; + opacity: 0.5; +} + +.user-profile-screen .user-profile-hero-avatar .avatar-fallback { + font-size: clamp(34px, 9vw, 58px); + font-weight: 800; +} + +.user-profile-metric { + width: min(100%, 92px); + min-width: 0; + padding: 0; + border: 0; + outline: 0; + background: transparent; + box-shadow: none; + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + color: #edf5ff; + cursor: pointer; +} + +.user-profile-metric:focus-visible .user-profile-metric-circle, +.user-profile-metric:active .user-profile-metric-circle { + transform: scale(0.94); +} + +.user-profile-metric-circle { + width: var(--user-profile-metric-size); + height: var(--user-profile-metric-size); + min-width: var(--user-profile-metric-size); + min-height: var(--user-profile-metric-size); + border-radius: 50%; + display: grid; + place-items: center; + border: 0; + background: rgba(14, 25, 43, 0.34); + box-shadow: none; + color: rgba(238, 246, 255, 0.94); + font-size: clamp(15px, 4.2vw, 19px); + font-weight: 800; + line-height: 1; + transition: transform 120ms ease, filter 160ms ease, box-shadow 160ms ease; +} + +.user-profile-metric.is-glowing .user-profile-metric-circle { + color: var(--app-topbar-gold); + background: rgba(14, 25, 43, 0.42); + box-shadow: + 0 0 8px var(--app-topbar-blue-glow), + 0 0 22px var(--app-topbar-blue-glow-soft), + 0 0 38px rgba(72, 145, 255, 0.18); + text-shadow: + 0 0 5px var(--app-topbar-blue-glow), + 0 0 14px var(--app-topbar-blue-glow-soft); +} + +.user-profile-metric-label { + width: 100%; + min-height: 24px; + color: rgba(205, 220, 243, 0.78); + font-size: clamp(9px, 2.8vw, 11px); + font-weight: 600; + line-height: 1.08; + text-align: center; +} + +.user-profile-metric.is-top-left, +.user-profile-metric.is-bottom-left { + grid-column: 1; + justify-self: end; + padding-right: clamp(4px, 2vw, 10px); +} + +.user-profile-metric.is-top-right, +.user-profile-metric.is-bottom-right { + grid-column: 3; + justify-self: start; + padding-left: clamp(4px, 2vw, 10px); +} + +/* Парные подписи читаются от центра: у левых к аватару прижат конец текста, у правых — начало. */ +.user-profile-hero .user-profile-metric.is-top-left .user-profile-metric-label, +.user-profile-hero .user-profile-metric.is-bottom-left .user-profile-metric-label { + text-align: right; + align-self: flex-end; +} + +.user-profile-hero .user-profile-metric.is-top-right .user-profile-metric-label, +.user-profile-hero .user-profile-metric.is-bottom-right .user-profile-metric-label { + text-align: left; + align-self: flex-start; +} + +.user-profile-metric.is-top-left, +.user-profile-metric.is-top-right { + grid-row: 1; + align-self: start; +} + +.user-profile-metric.is-bottom-left, +.user-profile-metric.is-bottom-right { + grid-row: 2; + align-self: end; + flex-direction: column-reverse; +} + +.user-profile-channel-metrics { + width: 66.666%; + margin: clamp(22px, 4vh, 30px) auto 0; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: start; + justify-items: center; +} + +.user-profile-channel-metrics .user-profile-metric { + width: 100%; +} + +.user-profile-channel-metrics .user-profile-metric-label { + min-height: 16px; + font-size: 11px; +} + +.user-profile-actions-wrap { + position: relative; + width: min(86%, 310px); + margin: clamp(18px, 3.4vh, 26px) auto 0; +} + +.user-profile-actions { + display: grid; + grid-template-columns: repeat(3, 1fr); + align-items: center; + justify-items: center; +} + +.user-profile-action-btn { + width: 54px; + height: 54px; + min-width: 54px; + min-height: 54px; + padding: 7px; + display: grid; + place-items: center; + border: 0 !important; + border-radius: 50%; + outline: 0; + background: transparent !important; + box-shadow: none !important; + color: rgba(226, 238, 255, 0.86); + cursor: pointer; + opacity: 0.9; + transition: transform 120ms ease, filter 160ms ease, opacity 160ms ease; +} + +.user-profile-action-btn:hover, +.user-profile-action-btn:focus-visible { + opacity: 1; + filter: drop-shadow(0 0 10px rgba(95, 196, 255, 0.42)); +} + +.user-profile-action-btn:active { + transform: scale(0.9); +} + +.user-profile-action-btn.is-active { + color: #d9f8ff; + filter: drop-shadow(0 0 9px rgba(83, 211, 255, 0.52)); +} + +.user-profile-action-btn img { + display: block; + width: 32px; + height: 32px; + object-fit: contain; +} + +.user-profile-action-btn.is-links img { + width: 39px; + height: 39px; + filter: brightness(1.08); +} + +.user-profile-action-svg { + width: 38px; + height: 38px; + overflow: visible; + fill: none; + stroke: currentColor; + stroke-width: 2.8; + stroke-linecap: round; + stroke-linejoin: round; +} + +.user-profile-add-menu { + position: absolute; + z-index: 12; + left: 0; + bottom: calc(100% + 8px); + width: min(190px, 68vw); + padding: 6px 4px; + border: 0; + border-radius: 18px; + background: linear-gradient(180deg, rgba(13, 20, 35, 0.93), rgba(5, 10, 20, 0.88)); + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.38); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + transform-origin: 28px 100%; + animation: user-profile-menu-in 130ms ease-out both; +} + +.user-profile-add-menu[hidden] { + display: none !important; +} + +.user-profile-add-menu.is-busy { + pointer-events: none; + opacity: 0.58; +} + +.user-profile-add-option { + width: 100%; + min-height: 42px; + padding: 8px 11px; + border: 0; + border-radius: 12px; + background: transparent; + box-shadow: none; + display: grid; + grid-template-columns: 1fr 24px; + align-items: center; + gap: 8px; + text-align: left; + color: rgba(231, 239, 255, 0.88); + font-size: 13px; + cursor: pointer; +} + +.user-profile-add-option:hover, +.user-profile-add-option:focus-visible, +.user-profile-add-option.is-current { + background: radial-gradient(circle at 18% 50%, rgba(70, 171, 224, 0.14), transparent 68%); + color: #ffffff; +} + +.user-profile-add-option-check { + color: #8deeff; + font-weight: 800; + text-align: center; + text-shadow: 0 0 9px rgba(89, 224, 255, 0.7); +} + +.user-profile-detail-links { + width: min(86%, 330px); + margin: clamp(16px, 3vh, 24px) auto 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 30px; +} + +.user-profile-detail-links button { + position: relative; + isolation: isolate; + min-height: 38px; + padding: 5px 8px; + border: 0 !important; + background: transparent !important; + box-shadow: none !important; + color: rgba(229, 239, 253, 0.86); + font-size: 15px; + font-weight: 650; + cursor: pointer; + text-shadow: + 0 0 4px rgba(92, 190, 255, 0.46), + 0 0 11px rgba(72, 145, 255, 0.2); +} + +.user-profile-detail-links button::before { + content: ''; + position: absolute; + z-index: -1; + left: 50%; + top: 50%; + width: 112px; + height: 34px; + transform: translate(-50%, -50%); + border-radius: 50%; + pointer-events: none; + background: radial-gradient(ellipse at center, rgba(92, 190, 255, 0.14) 0%, rgba(72, 145, 255, 0.07) 46%, transparent 74%); + filter: blur(4px); +} + +.user-profile-detail-links button:hover, +.user-profile-detail-links button:focus-visible { + color: #f2f7ff; + text-shadow: 0 0 12px rgba(111, 189, 255, 0.34); +} + +.user-profile-sheet-backdrop { + position: fixed; + z-index: 1400; + inset: 0; + display: flex; + align-items: flex-end; + justify-content: center; + padding: 0 max(10px, env(safe-area-inset-right)) max(10px, env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-left)); + background: rgba(0, 0, 0, 0.34); + backdrop-filter: blur(3px); + -webkit-backdrop-filter: blur(3px); +} + +.user-profile-sheet { + width: min(100%, 410px); + max-height: min(58vh, 480px); + overflow: auto; + padding: 10px 20px calc(22px + env(safe-area-inset-bottom)); + border: 0; + border-radius: 28px 28px 18px 18px; + background: + radial-gradient(circle at 50% -12%, rgba(73, 145, 211, 0.16), transparent 42%), + linear-gradient(180deg, rgba(13, 20, 34, 0.96), rgba(5, 9, 17, 0.97)); + box-shadow: 0 -20px 55px rgba(0, 0, 0, 0.46); + animation: user-profile-sheet-in 180ms ease-out both; +} + +.user-profile-sheet-handle { + width: 42px; + height: 4px; + margin: 1px auto 14px; + border-radius: 999px; + background: rgba(211, 226, 246, 0.2); +} + +.user-profile-sheet-title { + margin-bottom: 16px; + color: #f2f7ff; + font-size: 19px; + font-weight: 800; + text-align: center; +} + +.user-profile-sheet-content { + display: grid; + gap: 13px; +} + +.user-profile-sheet-copy { + white-space: pre-wrap; + overflow-wrap: anywhere; + color: rgba(225, 234, 248, 0.88); + font-size: 14px; + line-height: 1.58; +} + +.user-profile-sheet-copy.is-muted { + color: rgba(194, 207, 229, 0.56); + text-align: center; +} + +.user-profile-contact-row { + display: grid; + grid-template-columns: minmax(70px, auto) minmax(0, 1fr); + gap: 14px; + align-items: baseline; +} + +.user-profile-contact-row span { + color: rgba(181, 198, 225, 0.56); + font-size: 12px; +} + +.user-profile-contact-row b { + overflow-wrap: anywhere; + color: rgba(233, 240, 252, 0.9); + font-size: 14px; + font-weight: 600; + text-align: right; +} + +@keyframes user-profile-menu-in { + from { opacity: 0; transform: translateY(8px) scale(0.96); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +@keyframes user-profile-sheet-in { + from { opacity: 0; transform: translateY(24px); } + to { opacity: 1; transform: translateY(0); } +} + +@media (max-height: 700px) { + .user-profile-channel-metrics { + margin-top: 18px; + } + .user-profile-actions-wrap { + margin-top: 16px; + } + .user-profile-detail-links { + margin-top: 14px; + } +} + +@media (max-width: 350px) { + .user-profile-screen { + --user-profile-metric-size: 40px; + } + .user-profile-metric-label { + font-size: 9px; + } + .user-profile-actions-wrap { + width: 92%; + } +} + +/* Боковые метрики: круг строго остаётся внутри высоты аватара, подпись может быть ниже. */ +.user-profile-hero > .user-profile-metric { + position: relative; + height: var(--user-profile-metric-size); + gap: 0; +} + +.user-profile-hero > .user-profile-metric .user-profile-metric-label { + position: absolute; + top: calc(100% + 5px); + left: 0; +} + +.user-profile-metric.is-bottom-left, +.user-profile-metric.is-bottom-right { + flex-direction: column; +} + +/* ===== Профиль другого пользователя: геометрия v4 (2026-09-01) ===== + * - однородный фон без локальных пятен; + * - аватар +10% относительно v3; + * - все малые круги +10% и строго симметричны по четвертям высоты аватара; + * - неактивные круги полностью прозрачные; + * - активное состояние даёт только внешний ореол невидимой окружности (эффект затмения); + * - подписи якорятся на собственных кругах и остаются в боковых колонках. + */ +.user-profile-screen { + --user-profile-avatar-size: min(36.96vw, 147px); + --user-profile-metric-size: clamp(46px, 13.2vw, 53px); + margin-top: 0; + background: transparent; +} + +.user-profile-screen::before { + display: none; + content: none; +} + +.user-profile-body { + padding-top: clamp(10px, 1.8vh, 16px); +} + +.user-profile-hero { + grid-template-columns: minmax(0, 1fr) var(--user-profile-avatar-size) minmax(0, 1fr); + grid-template-rows: repeat(2, minmax(0, 1fr)); + height: var(--user-profile-avatar-size); + margin-top: 0; + overflow: visible; +} + +/* Боковые метрики имеют ровно размер своего невидимого круга. + Это исключает разные смещения слева/справа из-за ширины текста. */ +.user-profile-hero > .user-profile-metric { + position: relative; + width: var(--user-profile-metric-size); + min-width: var(--user-profile-metric-size); + height: var(--user-profile-metric-size); + padding: 0; + gap: 0; + justify-self: center; + align-self: center; + overflow: visible; +} + +.user-profile-metric.is-top-left, +.user-profile-metric.is-bottom-left { + grid-column: 1; + padding-right: 0; +} + +.user-profile-metric.is-top-right, +.user-profile-metric.is-bottom-right { + grid-column: 3; + padding-left: 0; +} + +.user-profile-metric.is-top-left, +.user-profile-metric.is-top-right { + grid-row: 1; + align-self: center; +} + +.user-profile-metric.is-bottom-left, +.user-profile-metric.is-bottom-right { + grid-row: 2; + align-self: center; + flex-direction: column; +} + +.user-profile-metric-circle { + position: relative; + z-index: 1; + width: var(--user-profile-metric-size); + height: var(--user-profile-metric-size); + min-width: var(--user-profile-metric-size); + min-height: var(--user-profile-metric-size); + border: 0 !important; + background: transparent !important; + box-shadow: none; + color: rgba(238, 246, 255, 0.94); + font-size: clamp(16px, 4.5vw, 20px); +} + +/* Свет идёт от геометрии невидимой окружности, а не от заливки/рамки. */ +.user-profile-metric.is-glowing .user-profile-metric-circle { + border: 0 !important; + background: transparent !important; + color: var(--app-topbar-gold); + box-shadow: + 0 0 7px 1px var(--app-topbar-blue-glow), + 0 0 20px 4px var(--app-topbar-blue-glow-soft), + 0 0 34px 8px rgba(72, 145, 255, 0.12); + text-shadow: none; +} + +/* Подпись всегда привязана к центру собственного круга и не влияет на его позицию. */ +.user-profile-hero > .user-profile-metric .user-profile-metric-label { + position: absolute; + top: calc(100% + 5px); + left: 50%; + width: clamp(76px, 22vw, 104px); + min-height: 0; + transform: translateX(-50%); + padding: 0; + text-align: center !important; + align-self: auto !important; + line-height: 1.05; + pointer-events: none; +} + +/* Чуть сильнее уводим подписи от аватара к внешним краям, + оставаясь связанными с центром соответствующего круга. */ +.user-profile-hero > .user-profile-metric.is-top-left .user-profile-metric-label, +.user-profile-hero > .user-profile-metric.is-bottom-left .user-profile-metric-label { + left: calc(50% - clamp(3px, 1.1vw, 5px)); +} + +.user-profile-hero > .user-profile-metric.is-top-right .user-profile-metric-label, +.user-profile-hero > .user-profile-metric.is-bottom-right .user-profile-metric-label { + left: calc(50% + clamp(3px, 1.1vw, 5px)); +} + +/* Нижние счётчики используют ту же прозрачную окружность и размер. */ +.user-profile-channel-metrics { + margin-top: clamp(28px, 4.6vh, 36px); +} + +.user-profile-channel-metrics .user-profile-metric { + gap: 5px; +} + +.user-profile-channel-metrics .user-profile-metric-label { + position: static; + width: 100%; + transform: none; + text-align: center; +} + +@media (max-height: 700px) { + .user-profile-body { + padding-top: 8px; + } + + .user-profile-channel-metrics { + margin-top: 25px; + } +} + +@media (max-width: 350px) { + .user-profile-screen { + --user-profile-avatar-size: min(36.96vw, 147px); + --user-profile-metric-size: 46px; + } + + .user-profile-hero > .user-profile-metric .user-profile-metric-label { + width: 72px; + font-size: 9px; + } +}