SHiNE-server/shine-UI/styles/network-graph.css
Pixel 345a21a211 Связи (эксперимент pixel-web): база — микро-взаимодействия + глубина
Фиксирую накопленные черновики как точку отката перед режимом «Интерактивная паутина»:
- press/pan-bend (резиновые нити при свайпе, тактильное «вдавливание» узла);
- глубина 2-3 уровней (прототип «Вселенная», переключатель в лаборатории);
- прогрессивное раскрытие (глубина скрыта, выплывает по нажатию/наведению).

Ветка pixel-web — экспериментальная (отдельно от pixel-08.06/PR), чтобы можно было откатиться.
Бамп client.version → 1.2.142.

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

541 lines
17 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: transform 160ms ease, 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);
}
/* Тактильный отклик «нажатия вглубь»: аватарка слегка вдавливается (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: rgba(160, 240, 255, 0.95);
box-shadow: 0 0 0 2px rgba(150, 238, 255, 0.6), 0 0 22px rgba(120, 230, 255, 0.85);
}
@media (prefers-reduced-motion: reduce) {
.fg-node.is-pressed .node-dot { transform: none; }
}
/* «Сияние» — мягкое живое свечение НА УЗЛЕ (аватарке), а не на линии связи.
Многослойная анимированная 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); }
/* === Глубина 2-3 уровней (прототип «Вселенная», только лаборатория) ============== */
/* 2-й уровень — «друзья друзей»: масштаб/прозрачность задаёт движок; тут убираем жирные тени
и делаем подпись мельче/тусклее, чтобы дальние узлы не спорили с основными. */
.fg-node.is-tier2 .node-dot {
border-color: rgba(150, 180, 220, 0.4);
box-shadow: none;
}
.fg-node.is-tier2 .fg-node-label {
font-size: 9px;
opacity: 0.55;
top: calc(100% + 1px);
}
/* 3-й уровень — микрозвезда: светящаяся точка без картинки (эффект далёкого созвездия). */
.fg-dot.is-tier3 {
width: 9px;
height: 9px;
border: 0;
background: radial-gradient(circle, #e6f6ff 0%, rgba(150, 215, 255, 0.95) 38%, rgba(120, 200, 255, 0) 75%);
box-shadow: 0 0 6px rgba(150, 220, 255, 0.9), 0 0 13px rgba(115, 200, 255, 0.5);
}
.fg-dot.is-tier3.is-shine {
background: radial-gradient(circle, #ffffff 0%, rgba(160, 240, 255, 1) 38%, rgba(120, 230, 255, 0) 75%);
box-shadow: 0 0 8px rgba(160, 240, 255, 1), 0 0 16px rgba(115, 220, 255, 0.7);
}
/* Чип-переключатель «Вселенная» — активное состояние наследует стиль .fg-filter-chip.is-active */
.fg-deep-chip.is-active {
background: rgba(150, 130, 255, 0.18);
border-color: rgba(190, 170, 255, 0.6);
box-shadow: inset 0 0.5px 0 rgba(255, 255, 255, 0.12), 0 0 14px rgba(150, 120, 255, 0.3);
color: #efeaff;
}
/* «Прицел» в центре экрана (зона фокуса) — позади узлов */
.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;
}