SHiNE-server/shine-UI/styles/network-graph.css
Pixel dc96033cb1 Связи: двухслойные линии-световоды, живой фон и стеклянные фильтры
- Сияющие связи — двухслойный неоновый «световод»: размытый glow (4px, blur 2px,
  opacity 0.4) + тонкий чёткий core (1.5px, #e0f7fc). Объёмное OLED-свечение,
  линия остаётся изящной. Оба слоя растут синхронно (общий dashoffset).
- Обычные линии — тоньше (1.0–1.2px) и глубокий уход в прозрачность (0.42 → 0.07),
  чтобы матовые связи не спорили с сияющими.
- Живой фон-«небула»: глубокое размытое сине-голубое облако под центром, медленная
  пульсация радиуса/яркости + переливы индиго↔ультрамарин (hue-rotate, 7с).
- Стеклянные чипы фильтров (frosted glass): rgba(255,255,255,0.03) + backdrop blur(12px)
  + граница 0.5px solid rgba(255,255,255,0.1); активный подсвечен сине-голубым.
- Бамп client.version → 1.2.138; документация фичи обновлена.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:23:17 +03:00

499 lines
15 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.
========================================================================== */
.fg-stage {
touch-action: none; /* перехватываем жесты сами (pan), без скролла страницы */
user-select: none;
-webkit-user-select: none;
cursor: grab;
background: radial-gradient(circle at 50% 42%, rgba(83, 216, 251, 0.07), rgba(255, 255, 255, 0.01) 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%, rgba(80, 150, 255, 0.30) 0%, rgba(60, 100, 220, 0.15) 42%, rgba(40, 70, 170, 0) 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; /* плавное появление линий при перестройке */
}
/* Сияющая связь = двухслойный неоновый «световод» (Neon Layering): изящно, но объёмно (как OLED).
GLOW — широкий размытый ореол неонового оттенка под линией; CORE — тонкий чёткий светлый контур. */
.fg-edge-glow {
fill: none;
stroke: rgba(110, 225, 255, 1);
stroke-width: 4;
stroke-linecap: round;
filter: blur(2px); /* мягкое объёмное свечение вокруг нити */
}
.fg-edge-core {
fill: none;
stroke: #e0f7fc; /* ультра-светлый голубой — чёткий контур луча */
stroke-width: 1.5;
stroke-linecap: round;
}
.fg-node {
position: absolute;
left: 0;
top: 0;
width: 52px;
height: 52px;
border: 0;
padding: 0;
background: transparent;
color: inherit;
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) */
.fg-node .node-dot {
width: 52px;
height: 52px;
margin: 0;
font-size: 16px;
transition: box-shadow 160ms ease, border-color 160ms ease;
}
.fg-node.is-family .node-dot {
background: linear-gradient(165deg, #785038, #5f3e2c);
border-color: rgba(255, 194, 143, 0.6);
}
.fg-node.is-friend .node-dot {
background: linear-gradient(165deg, #2f4f80, #2a3f62);
border-color: rgba(150, 190, 255, 0.5);
}
.fg-node.is-business .node-dot {
background: linear-gradient(165deg, #4a3b7a, #2f2750);
border-color: rgba(196, 165, 255, 0.55);
}
.fg-node.is-contact .node-dot {
background: linear-gradient(165deg, #36435c, #283142);
border-color: rgba(180, 200, 226, 0.4);
}
.fg-node.is-focus .node-dot {
background: linear-gradient(130deg, #3a5f8e, #3dc4df);
color: #061119;
border-color: rgba(180, 230, 255, 0.85);
box-shadow: 0 0 0 2px rgba(143, 231, 255, 0.45), 0 10px 22px rgba(4, 8, 15, 0.45);
}
.fg-node:focus-visible .node-dot,
.fg-node:hover .node-dot {
border-color: rgba(166, 218, 255, 0.95);
box-shadow: 0 0 0 3px rgba(77, 160, 255, 0.22), 0 8px 16px rgba(4, 8, 15, 0.35);
}
/* «Сияние» — мягкое живое свечение НА УЗЛЕ (аватарке), а не на линии связи.
Многослойная анимированная box-shadow + размытый радиальный ореол (через внешний SVG-фильтр).
Пульсация очень медленная и плавная (3.6с): радиус и прозрачность «дышат» 0.5 ↔ 1.0 —
как мягкое свечение живого организма в темноте, а не «жирный маркер». */
.fg-node.is-shine .node-dot {
border-color: rgba(150, 240, 255, 0.62);
animation: fg-shine-glow 3.6s ease-in-out infinite;
}
/* размытый радиальный ореол позади аватарки; внешний SVG-фильтр даёт мягкое гауссово размытие */
.fg-node.is-shine .node-dot::before {
content: '';
position: absolute;
inset: -12px;
border-radius: 50%;
background: radial-gradient(circle, rgba(140, 240, 255, 0.5) 0%, rgba(130, 235, 255, 0.18) 46%, rgba(130, 235, 255, 0) 72%);
filter: url(#fg-shine-glow);
z-index: -1;
pointer-events: none;
animation: fg-shine-halo 3.6s ease-in-out infinite;
}
/* пульсация многослойной тени: компактное приглушённое → широкое мягкое свечение */
@keyframes fg-shine-glow {
0%, 100% {
box-shadow:
0 0 5px rgba(125, 232, 255, 0.30),
0 0 11px rgba(112, 226, 255, 0.18),
0 0 20px rgba(100, 220, 255, 0.10);
}
50% {
box-shadow:
0 0 9px rgba(150, 245, 255, 0.62),
0 0 20px rgba(122, 236, 255, 0.42),
0 0 36px rgba(100, 220, 255, 0.26);
}
}
/* ореол дышит размером и прозрачностью синхронно с тенью (мягко, без рывков) */
@keyframes fg-shine-halo {
0%, 100% { transform: scale(0.9); opacity: 0.5; }
50% { transform: scale(1.12); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.fg-node.is-shine .node-dot { animation: none; }
.fg-node.is-shine .node-dot::before { animation: none; }
}
/* мягкое свечение вокруг фокуса (статичное; «дышит» вместе с размером узла ниже) */
.fg-node.is-focus .node-dot::after {
content: '';
position: absolute;
inset: -12px;
border-radius: 50%;
background: radial-gradient(circle, rgba(130, 235, 255, 0.32) 0%, rgba(130, 235, 255, 0) 70%);
z-index: -1;
pointer-events: none;
}
/* «Дыхание» фокуса — бесконечная очень мягкая пульсация РАЗМЕРА (база 1.5x → 1.481.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: 10px;
line-height: 1.1;
color: #d6e2ff;
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: rgba(8, 14, 24, 0.74);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
color: #f4f8ff;
font-weight: 600;
text-shadow: none;
}
/* Лёгкая точка (узлы сверх хард-лимита DOM) — без аватара/подписи */
.fg-dot {
width: 15px;
height: 15px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.25);
background: #36435c;
box-shadow: 0 2px 6px rgba(4, 8, 15, 0.4);
}
.fg-dot.is-family { background: #6f4a34; border-color: rgba(255, 194, 143, 0.5); }
.fg-dot.is-friend { background: #2f4f80; border-color: rgba(150, 190, 255, 0.45); }
.fg-dot.is-business { background: #4a3b7a; border-color: rgba(196, 165, 255, 0.5); }
.fg-dot.is-contact { background: #36435c; }
.fg-dot.is-shine { box-shadow: 0 0 9px rgba(130, 235, 255, 0.75); }
/* «Прицел» в центре экрана (зона фокуса) — позади узлов */
.fg-reticle {
position: absolute;
left: 50%;
top: 50%;
width: 64px;
height: 64px;
margin: -32px 0 0 -32px;
border-radius: 50%;
border: 2px dashed rgba(150, 190, 255, 0.3);
pointer-events: none;
z-index: 0;
opacity: 0.45;
transition: width 200ms ease, height 200ms ease, margin 200ms ease, border-color 200ms ease, opacity 200ms ease;
}
.fg-reticle.is-locked {
width: 94px;
height: 94px;
margin: -47px 0 0 -47px;
border-color: rgba(130, 235, 255, 0.65);
opacity: 0.85;
}
/* «Призрак» старой карты при 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-reticle.is-pulse {
animation: fg-reticle-pulse 0.6s ease;
}
@keyframes fg-reticle-pulse {
0% { transform: scale(1); }
40% { transform: scale(1.22); border-color: rgba(130, 235, 255, 0.9); }
100% { transform: scale(1); }
}
/* Панель фильтров слоёв (оверлей под шапкой) */
.fg-filter-bar {
position: absolute;
top: max(54px, calc(env(safe-area-inset-top) + 50px));
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) */
.fg-filter-chip {
pointer-events: auto;
border: 0.5px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
color: #cfe0ff;
font-size: 12px;
font-weight: 600;
line-height: 1;
padding: 7px 14px;
border-radius: 999px;
cursor: pointer;
box-shadow: inset 0 0.5px 0 rgba(255, 255, 255, 0.08); /* лёгкий стеклянный блик сверху */
transition: background 160ms ease, border-color 160ms ease, color 160ms ease, box-shadow 160ms ease;
}
/* Активный таб — то же стекло, но подсвеченное сине-голубым (в тон неону графа) */
.fg-filter-chip.is-active {
background: rgba(125, 215, 255, 0.16);
border-color: rgba(160, 230, 255, 0.55);
color: #eaf7ff;
box-shadow: inset 0 0.5px 0 rgba(255, 255, 255, 0.12), 0 0 14px rgba(110, 210, 255, 0.28);
}
/* Контекстное меню узла (долгое нажатие) — в #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: rgba(16, 24, 40, 0.97);
border: 1px solid rgba(166, 196, 245, 0.28);
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 rgba(255, 255, 255, 0.08);
margin-bottom: 3px;
}
.fg-menu-login {
font-weight: 700;
color: #eaf1ff;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fg-menu-rel {
font-size: 11px;
color: #9fb6e0;
flex: 0 0 auto;
}
.fg-menu-item {
text-align: left;
background: transparent;
border: 0;
color: #dfe9ff;
font-size: 14px;
padding: 9px 10px;
border-radius: 9px;
cursor: pointer;
}
.fg-menu-item:hover {
background: rgba(77, 160, 255, 0.16);
}
.fg-menu-item.is-stub {
color: #7f8aa3;
cursor: default;
}
.fg-menu-item.is-stub:hover {
background: transparent;
}
/* Нижний сниппет (bottom sheet) */
.fg-sheet {
position: absolute;
left: 12px;
right: 12px;
bottom: calc(12px + env(safe-area-inset-bottom));
z-index: 13;
display: none;
padding: 12px 14px 14px;
background: rgba(16, 24, 40, 0.95);
border: 1px solid rgba(166, 196, 245, 0.26);
border-radius: 16px;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.fg-sheet.is-open {
display: block;
animation: fg-sheet-in 200ms ease;
}
@keyframes fg-sheet-in {
from { transform: translateY(16px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.fg-sheet-close {
position: absolute;
top: 8px;
right: 10px;
background: transparent;
border: 0;
color: #9fb6e0;
font-size: 16px;
cursor: pointer;
line-height: 1;
}
.fg-sheet-title {
font-weight: 700;
font-size: 15px;
color: #eaf1ff;
display: flex;
gap: 8px;
align-items: center;
}
.fg-sheet-badge {
font-size: 10px;
background: rgba(130, 235, 255, 0.2);
color: #bff0ff;
padding: 2px 8px;
border-radius: 999px;
font-weight: 600;
}
.fg-sheet-rel {
font-size: 12px;
color: #9fb6e0;
margin-top: 3px;
}
.fg-sheet-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.fg-sheet-actions > button {
flex: 1;
}