Files
SHiNE-server/shine-UI/styles/network-graph.css
T
qandClaude Opus 5.5 f8900e531a UI: новый общий стиль, тёмная/светлая тема, настраиваемая палитра
- styles/main.css: роли цветов (по умолчанию «Индиго»), шкала отступов/скруглений/шрифтов,
  цвета отношений для обеих тем; жёсткие цвета в стилях заменены на роли.
- Палитра: пресеты и личные правки, «Оформление» Авто/День/Ночь, долгое нажатие —
  редактор цветов с экспортом/импортом.
- Каналы: пузыри постов автора, плашки дней, строка «Написать в канал…», «О канале»,
  создание канала с адресом из названия; лента открывается на свежих постах.
- Чаты: плоский список, чипы-фильтры, пузыри, плашки дней; нижняя панель скрыта в переписке.
- Корневые разделы — единая шапка; профиль и чужой профиль — общая карточка;
  настройки, кошелёк, сеансы — меню-списки.
- Нижняя панель: иконки без подписей, бейджи на иконках.
- confirmDialog вместо window.confirm/alert; на телефоне диалоги — шторки снизу.
- docs/UI-Design/ISSUES-for-dev.md — найденные ошибки сервера/UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 10:05:26 +03:00

595 lines
24 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================================
Force-directed карта связей (.fg-*) — интерактивный граф на странице «Связи».
Узлы позиционируются трансформами (GPU), рёбра — отдельный SVG-слой.
Отдельный модуль, чтобы не раздувать components.css.
========================================================================== */
/* Цвета отношений (--rel-*) объявлены в styles/main.css — отдельно для дневной и ночной темы. */
.fg-stage {
touch-action: none; /* перехватываем жесты сами (pan), без скролла страницы */
user-select: none;
-webkit-user-select: none;
cursor: grab;
background: radial-gradient(circle at 50% 42%, color-mix(in srgb, var(--rel-shining) 7%, transparent), color-mix(in srgb, var(--text-primary) 1%, transparent) 60%);
}
.fg-stage:active {
cursor: grabbing;
}
/* Живой фон-«небула»: глубокое размытое сине-голубое облако света строго под центральным узлом.
Медленно «дышит» (радиус/яркость) и переливается индиго↔ультрамарин (hue-rotate) за 7с.
Чистый CSS на компоновщике — создаёт ощущение живой светящейся среды, не будит rAF-цикл. */
.fg-stage::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle 320px at 50% 47%, color-mix(in srgb, var(--rel-contact) 30%, transparent) 0%, color-mix(in srgb, var(--rel-contact) 15%, transparent) 42%, color-mix(in srgb, var(--rel-contact) 0%, transparent) 72%);
filter: blur(80px);
pointer-events: none;
z-index: 0; /* строго под линиями (z:0, но раньше по порядку) и узлами (z:1) */
animation: fg-nebula 7s ease-in-out infinite;
}
@keyframes fg-nebula {
0%, 100% { opacity: 0.70; filter: blur(80px) hue-rotate(-12deg); }
50% { opacity: 1.00; filter: blur(96px) hue-rotate(16deg); }
}
@media (prefers-reduced-motion: reduce) {
.fg-stage::before { animation: none; }
}
.fg-world {
position: absolute;
left: 50%;
top: 50%;
width: 0;
height: 0;
will-change: transform;
z-index: 1; /* узлы и подписи строго над линиями связей */
}
.fg-edges {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: visible;
z-index: 0; /* линии связей — под узлами */
transition: opacity 420ms ease; /* плавное появление линий при перестройке */
}
/* Сияющая связь = плазменный композитинг (3 слоя на ОДНОМ S-пути, см. renderEdges).
Настоящий НЕОН: яркое светлое ядро + ВИДИМЫЙ голубой ореол вокруг. Слои поля/трубки идут в режиме
mix-blend-mode: screen → свет складывается аддитивно с тёмным фоном (как реальное свечение), а в точках
пересечения нитей у центра — ярче (энергетический хаб). Прозрачность слоёв — inline (×spotlight/глубину). */
.fg-plasma-flare { /* нижний: широкое насыщенное голубое плазменное свечение (по референсу) */
fill: none;
stroke: var(--rel-shining); /* глубокий голубой */
stroke-width: 16;
stroke-linecap: round;
filter: url(#fg-plasma-blur6); /* мягкое объёмное свечение (гладкие края — как на референсе) */
mix-blend-mode: screen; /* аддитивное свечение поверх тёмного фона */
/* синхро-«дыхание» поля толщиной в такт ободку сияющего узла (3.6с); прозрачность не трогаем (inline) */
animation: fg-plasma-breath 3.6s ease-in-out infinite;
}
.fg-plasma-tube { /* средний: яркая толстая неоновая трубка */
fill: none;
stroke: var(--rel-shining); /* яркий циан */
stroke-width: 6;
stroke-linecap: round;
filter: url(#fg-plasma-blur2); /* SVG feGaussianBlur stdDeviation=2 */
mix-blend-mode: screen; /* аддитивное свечение */
}
.fg-plasma-core { /* верхний: яркое чёткое ядро (светло-голубо-белое) */
fill: none;
stroke: var(--text-primary); /* светло-голубо-белое — «жидкое» ядро */
stroke-width: 2;
stroke-linecap: round;
}
/* мягкое «дыхание» плазменного облака толщиной, синхронно с пульсом сияющего ободка (3.6с) */
@keyframes fg-plasma-breath {
0%, 100% { stroke-width: 14; }
50% { stroke-width: 19; }
}
@media (prefers-reduced-motion: reduce) {
.fg-plasma-flare { animation: none; }
}
.fg-node {
position: absolute;
left: 0;
top: 0;
width: 52px;
height: 52px;
border: 0;
padding: 0;
background: transparent;
cursor: pointer;
transform-origin: center center;
will-change: transform;
touch-action: none;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none; /* iOS: не показывать системное меню по долгому тапу */
z-index: 1;
}
/* круглая аватарка узла (переиспользуем существующий .node-dot из renderUserAvatar) */
/* 58px → радиус 29 = ORB_R в force-graph.js (контакт линий берётся от этого радиуса). */
.fg-node .node-dot {
width: 58px;
height: 58px;
margin: 0;
font-size: 16px;
transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
}
/* SVG-«стеклянный орб» — масштабируем так, чтобы сфера (r42 = 84% SVG) ≈ диаметр узла → линии-связи
прилипают к краю орба, как раньше. Хост .node-dot держит размер/состояния/синхронизацию позиций. */
.fg-node .node-dot.fg-orb-host {
position: relative;
background: none;
overflow: visible; /* не срезать внешнее свечение орба */
box-shadow: none;
}
/* A/B PNG-оверлей орба (ветка glass-png-overlay): фото снизу + запечённый стеклянный PNG сверху.
Бокс = 119% от .node-dot (как .fg-orb-svg → сфера ≈ кромке node-dot, контакт линий от ORB_R сохраняется). */
/* Специфичность `.fg-orb-host …` бьёт глобальное `.node-dot img` (иначе оно гасит opacity→0
и форсит размер 100%). Поэтому opacity/размеры/радиус задаём здесь явно. */
/* Кромку даёт сам glass_overlay.png → убираем остаточный border .node-dot (синее кольцо
старого векторного орба). Только у PNG-хоста; вектор и свечение/box-shadow не трогаем. */
/* (0,4,0) — выше специфичности правил категории `.fg-node.is-family .node-dot` (0,3,0),
иначе их background/border перебивают. */
.fg-node .node-dot.fg-orb-host:has(.fg-pngorb) {
border: none;
background: none; /* фон-градиент категории не торчит из-под прозрачного стекла (та самая «обводка») */
}
.fg-orb-host .fg-pngorb {
position: absolute;
left: 50%; top: 50%;
width: 119%; height: 119%;
transform: translate(-50%, -50%);
}
.fg-orb-host .fg-pngorb-glass {
position: absolute;
left: 50%; top: 50%;
width: 100%; height: 100%;
transform: translate(-50%, -50%);
opacity: 1;
border-radius: 0;
object-fit: contain;
display: block;
pointer-events: none;
}
.fg-orb-host .fg-pngorb-photo {
position: absolute;
left: 50%; top: 50%;
width: 78%; height: 78%; /* ~78% от бокса оверлея — сидит внутри стеклянной кромки */
transform: translate(-50%, -50%);
opacity: 1;
border-radius: 50%; /* фолбэк, если mask не поддержан */
object-fit: cover;
display: block;
/* Мягкий край: фото непрозрачно до --feather-full, плавно гаснет к 0 у --feather-edge —
сливается со стеклом без жёсткого ободка. Силу растушёвки крутим этими двумя параметрами. */
--feather-full: 62%;
--feather-edge: 78%;
-webkit-mask-image: radial-gradient(circle at 50% 50%, var(--surface) var(--feather-full), transparent var(--feather-edge));
mask-image: radial-gradient(circle at 50% 50%, var(--surface) var(--feather-full), transparent var(--feather-edge));
}
.fg-orb-host .fg-pngorb-init {
display: flex; align-items: center; justify-content: center;
background: var(--surface); color: var(--text-primary); font-weight: 600; font-size: 20px;
}
/* Маленький знак официального пользователя. Он находится внутри .node-dot, поэтому автоматически
масштабируется вместе с аватаркой при focus/zoom/анимациях графа. */
/* ВАЖНО: селектор намеренно специфичнее глобального `.node-dot img` из features/network.css,
где обычные фото стартуют с opacity:0 и width/height:100%. Иначе badge тоже наследует эти
правила и становится полностью невидимым. */
.fg-node .node-dot .fg-official-badge {
position: absolute;
left: -2%;
bottom: -1%;
width: 32%;
height: 32%;
min-width: 18px;
min-height: 18px;
object-fit: contain;
object-position: center;
display: block;
opacity: 1;
border-radius: 0;
transition: none;
z-index: 20;
pointer-events: none;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.42));
}
.fg-node.is-family .node-dot {
background: linear-gradient(165deg, var(--rel-family), var(--surface));
border-color: color-mix(in srgb, var(--rel-family) 60%, transparent);
}
.fg-node.is-friend .node-dot {
background: linear-gradient(165deg, var(--rel-contact), var(--surface));
border-color: color-mix(in srgb, var(--rel-contact) 50%, transparent);
}
.fg-node.is-business .node-dot {
background: linear-gradient(165deg, var(--rel-contact), var(--surface));
border-color: color-mix(in srgb, var(--rel-contact) 55%, transparent);
}
.fg-node.is-contact .node-dot {
background: linear-gradient(165deg, var(--surface), var(--surface));
border-color: color-mix(in srgb, var(--rel-shining) 40%, transparent);
}
.fg-node.is-focus .node-dot {
background: linear-gradient(130deg, var(--rel-shining), var(--rel-shining));
color: var(--on-accent);
border-color: color-mix(in srgb, var(--rel-shining) 85%, transparent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--rel-shining) 45%, transparent), 0 10px 22px rgba(4, 8, 15, 0.45);
}
/* Тактильный отклик «нажатия вглубь»: аватарка слегка вдавливается (scale 0.92), а неоновое кольцо
вспыхивает заметно ярче (~1.5×). Срабатывает при наведении, фокусе и зажатии (.is-pressed). */
.fg-node:focus-visible .node-dot,
.fg-node:hover .node-dot,
.fg-node.is-pressed .node-dot {
transform: scale(0.92);
border-color: color-mix(in srgb, var(--rel-shining) 95%, transparent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--rel-shining) 60%, transparent), 0 0 22px color-mix(in srgb, var(--rel-shining) 85%, transparent);
}
@media (prefers-reduced-motion: reduce) {
.fg-node.is-pressed .node-dot { transform: none; }
}
/* «Сияние» на аватарке теперь стационарное. Интенсивность зафиксирована примерно на 2/3 пути
от прежнего минимального состояния к максимальному: заметно, но без постоянного «дыхания». */
.fg-node.is-shine .node-dot {
border-color: color-mix(in srgb, var(--rel-shining) 62%, transparent);
box-shadow:
0 0 8.3px color-mix(in srgb, var(--rel-shining) 57%, transparent),
0 0 18.3px color-mix(in srgb, var(--rel-shining) 39%, transparent),
0 0 33px color-mix(in srgb, var(--rel-shining) 24%, transparent);
}
/* Статичный размытый ореол позади аватарки — та же промежуточная интенсивность ~2/3. */
.fg-node.is-shine .node-dot::before {
content: '';
position: absolute;
inset: -12px;
border-radius: 50%;
background: radial-gradient(circle, color-mix(in srgb, var(--rel-shining) 50%, transparent) 0%, color-mix(in srgb, var(--rel-shining) 18%, transparent) 46%, color-mix(in srgb, var(--rel-shining) 0%, transparent) 72%);
filter: url(#fg-shine-glow);
z-index: -1;
pointer-events: none;
transform: scale(1.07);
opacity: 0.91;
}
/* мягкое свечение вокруг фокуса (статичное; «дышит» вместе с размером узла ниже) */
.fg-node.is-focus .node-dot::after {
content: '';
position: absolute;
inset: -12px;
border-radius: 50%;
background: radial-gradient(circle, color-mix(in srgb, var(--rel-shining) 32%, transparent) 0%, color-mix(in srgb, var(--rel-shining) 0%, transparent) 70%);
z-index: -1;
pointer-events: none;
}
/* «Дыхание» фокуса — бесконечная очень мягкая пульсация РАЗМЕРА (база 1.5x → 1.48–1.52x),
период 4с. CSS-анимация на transform (GPU) — НЕ будит rAF-цикл физики; интерфейс «живой». */
.fg-node.is-focus .node-dot {
animation: fg-focus-breath 4s ease-in-out infinite;
}
@keyframes fg-focus-breath {
0%, 100% { transform: scale(0.987); }
50% { transform: scale(1.013); }
}
@media (prefers-reduced-motion: reduce) {
.fg-node.is-focus .node-dot { animation: none; }
}
/* подпись под узлом — абсолютная, чтобы не влиять на размер бокса (центрирование по трансформу) */
.fg-node-label {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 5px;
max-width: 110px;
font-size: var(--text-xs, 12px);
font-weight: var(--weight-medium);
line-height: 1.1;
color: var(--text-primary);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.55);
pointer-events: none;
}
.fg-node.is-secondary .fg-node-label {
opacity: 0.75;
}
/* Имя центрального узла — на подложке, чтобы линии связей не просвечивали сквозь текст */
.fg-node.is-focus .fg-node-label {
margin-top: 7px;
padding: 3px 10px;
border-radius: 9px;
background: var(--surface);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
color: var(--text-primary);
font-weight: 600;
text-shadow: none;
}
/* Лёгкая точка (узлы сверх хард-лимита DOM) — без аватара/подписи */
.fg-dot {
width: 15px;
height: 15px;
border-radius: 50%;
border: 1px solid var(--border-subtle);
background: var(--surface);
box-shadow: 0 2px 6px rgba(4, 8, 15, 0.4);
}
.fg-dot.is-family { background: var(--rel-family); border-color: color-mix(in srgb, var(--rel-family) 50%, transparent); }
.fg-dot.is-friend { background: var(--rel-contact); border-color: color-mix(in srgb, var(--rel-contact) 45%, transparent); }
.fg-dot.is-business { background: var(--rel-contact); border-color: color-mix(in srgb, var(--rel-contact) 50%, transparent); }
.fg-dot.is-contact { background: var(--surface); }
.fg-dot.is-shine { box-shadow: 0 0 9px color-mix(in srgb, var(--rel-shining) 75%, transparent); }
/* === Глубина 2-3 уровней (прототип «Вселенная», только лаборатория) ============== */
/* 2-й уровень — «друзья друзей»: полноценная аватарка (лицо + имя), просто мельче основных.
Масштаб/прозрачность задаёт движок; здесь — читаемый ободок и подпись (не «дырка»). */
.fg-node.is-tier2 .node-dot {
border-color: color-mix(in srgb, var(--rel-shining) 65%, transparent);
box-shadow: 0 2px 8px rgba(4, 8, 15, 0.4);
}
.fg-node.is-tier2 .fg-node-label {
font-size: 11px;
opacity: 0.9;
top: calc(100% + 1px);
}
/* 3-й уровень — микрозвезда: светящаяся точка без картинки (эффект далёкого созвездия). */
.fg-dot.is-tier3 {
width: 9px;
height: 9px;
border: 0;
background: radial-gradient(circle, var(--text-primary) 0%, color-mix(in srgb, var(--rel-shining) 95%, transparent) 38%, color-mix(in srgb, var(--rel-shining) 0%, transparent) 75%);
box-shadow: 0 0 6px color-mix(in srgb, var(--rel-shining) 90%, transparent), 0 0 13px color-mix(in srgb, var(--rel-shining) 50%, transparent);
/* медленное мерцание «звезды» — по box-shadow/яркости (НЕ opacity/scale: ими управляет движок при
раскрытии). У каждой звезды своя задержка (inline animation-delay) → живое созвездие. */
animation: fg-star-twinkle 3.4s ease-in-out infinite;
}
.fg-dot.is-tier3.is-shine {
background: radial-gradient(circle, var(--text-primary) 0%, var(--rel-shining) 38%, color-mix(in srgb, var(--rel-shining) 0%, transparent) 75%);
box-shadow: 0 0 8px var(--rel-shining), 0 0 16px color-mix(in srgb, var(--rel-shining) 70%, transparent);
}
@keyframes fg-star-twinkle {
0%, 100% { box-shadow: 0 0 3px color-mix(in srgb, var(--rel-shining) 45%, transparent), 0 0 7px color-mix(in srgb, var(--rel-shining) 25%, transparent); filter: brightness(0.78); }
50% { box-shadow: 0 0 7px color-mix(in srgb, var(--rel-shining) 95%, transparent), 0 0 15px color-mix(in srgb, var(--rel-shining) 60%, transparent); filter: brightness(1.3); }
}
@media (prefers-reduced-motion: reduce) {
.fg-dot.is-tier3 { animation: none; }
}
/* «Призрак» старой карты при Z-переходе (эффект погружения) */
.fg-ghost-layer {
position: absolute;
inset: 0; /* полноэкранный overlay → клон линий/узлов совпадает по координатам */
pointer-events: none;
z-index: 0;
opacity: 0.5; /* стартовая прозрачность шлейфа выше → дольше читается (JS уводит в 0) */
transform-origin: 50% 50%;
/* лениво и породисто тает на месте за 1000мс — медленный дорогой шлейф истории перехода */
transition: transform 1000ms cubic-bezier(0.16, 1, 0.3, 1), opacity 1000ms ease;
}
/* Панель фильтров слоёв (оверлей под шапкой) */
.fg-filter-bar {
position: absolute;
top: max(72px, calc(env(safe-area-inset-top) + 68px));
left: 0;
right: 0;
z-index: 11;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
padding: 0 12px;
pointer-events: none;
}
/* Стеклянные табы — тонкие пластины матового стекла (frosted glass) */
/* Служебные переключатели карты: история и X2 намеренно чуть компактнее основных фильтров. */
/* Контекстное меню узла (долгое нажатие) — в #modal-root, поверх всего, не масштабируется */
.fg-menu-overlay {
position: fixed;
inset: 0;
z-index: 50;
}
.fg-menu {
position: fixed;
min-width: 210px;
padding: 8px;
display: grid;
gap: 3px;
background: var(--surface);
border: 1px solid color-mix(in srgb, var(--rel-contact) 28%, transparent);
border-radius: 14px;
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.55);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
z-index: 51;
}
.fg-menu-head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 10px;
padding: 4px 8px 8px;
border-bottom: 1px solid var(--border-subtle);
margin-bottom: 3px;
}
.fg-menu-login {
font-weight: 700;
color: var(--text-primary);
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fg-menu-rel {
font-size: 11px;
color: var(--rel-contact);
flex: 0 0 auto;
}
.fg-menu-item {
text-align: left;
background: transparent;
border: 0;
font-size: 14px;
padding: 9px 10px;
border-radius: 9px;
cursor: pointer;
}
.fg-menu-item.is-stub {
cursor: default;
}
.fg-menu-item.is-stub:hover {
background: transparent;
}
/* === Партия 2: поиск, хлебные крошки, цветовые кластеры ============================== */
/* Кластерная аура по категории удалена (цветной фон/обводка узла убраны). Сияющим/фокусу
box-shadow не навязываем — у них свой эффект свечения (is-shine/is-focus выше). */
.fg-node.is-focus:not(.is-shine) .node-dot { box-shadow: none; }
/* Строка поиска (оверлей вверху, под панелью фильтров) */
.fg-search {
position: absolute;
top: max(92px, calc(env(safe-area-inset-top) + 88px));
left: 50%;
transform: translateX(-50%);
z-index: 12;
width: min(280px, 70vw);
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 999px;
background: color-mix(in srgb, var(--text-primary) 4%, transparent);
border: 0.5px solid var(--border-subtle);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: inset 0 0.5px 0 color-mix(in srgb, var(--text-primary) 8%, transparent);
}
.fg-search input {
flex: 1;
border: 0;
background: transparent;
color: var(--text-primary);
font-size: 13px;
outline: none;
}
.fg-search input::placeholder { color: var(--text-primary); }
.fg-search .fg-search-ico { color: var(--rel-contact); font-size: 13px; }
/* Хлебные крошки навигации (стек погружений: Иван › Нина › Ада) */
.fg-breadcrumb {
position: absolute;
top: max(132px, calc(env(safe-area-inset-top) + 128px));
left: 0;
right: 0;
z-index: 12;
display: none;
justify-content: center;
flex-wrap: wrap;
gap: 4px;
padding: 0 12px;
pointer-events: none;
}
.fg-breadcrumb.is-open { display: flex; }
.fg-crumb {
pointer-events: auto;
border: 0;
background: var(--surface);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: var(--text-primary);
font-size: 11px;
font-weight: 600;
line-height: 1;
padding: 5px 10px;
border-radius: 999px;
cursor: pointer;
max-width: 110px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fg-crumb.is-last {
background: color-mix(in srgb, var(--rel-shining) 18%, transparent);
color: var(--text-primary);
cursor: default;
}
.fg-crumb-sep { color: var(--on-accent); font-size: 11px; align-self: center; pointer-events: none; }
/* Доступность: визуально скрытый список графа для скринридеров (sr-only, читается ассистивными технологиями) */
.fg-a11y {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
/* «Общая связь» (этот человек — и твой друг тоже): золотой ободок. Значок ★ убран по запросу. */
.fg-node.is-common .node-dot {
border-color: transparent;
box-shadow: 0 0 14px color-mix(in srgb, var(--rel-family) 40%, transparent);
}