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>
This commit is contained in:
q
2026-09-26 10:05:26 +03:00
co-authored by Claude Opus 5.5
parent 6e5b57fd7c
commit f8900e531a
94 changed files with 3337 additions and 2432 deletions
+69 -111
View File
@@ -4,26 +4,14 @@
Отдельный модуль, чтобы не раздувать components.css.
========================================================================== */
/* Канонические токены ЯЗЫКА СВЯЗЕЙ (единый источник цвета отношений для всего продукта).
Экран «Личные сообщения» наследует их через --dm-* (не дублирует hex).
(force-graph пока использует свои JS-цвета RELATION_COLORS — миграция на токены = будущая задача.) */
:root {
--rel-contact: #8C63FF; /* violet — обычная связь / контакт */
--rel-family: #F0B82E; /* gold — семья / близкий круг / важность / подтверждение */
--rel-shining: #68D8FF; /* celestial — сияющий / сильная активная связь */
--rel-link: #19E58A; /* emerald — статус «Связь» (активный канал) */
--rel-contact-glow: rgba(140, 99, 255, 0.24);
--rel-family-glow: rgba(240, 184, 46, 0.30);
--rel-shining-glow: rgba(104, 216, 255, 0.35);
--rel-link-glow: rgba(25, 229, 138, 0.24);
}
/* Цвета отношений (--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%, rgba(83, 216, 251, 0.07), rgba(255, 255, 255, 0.01) 60%);
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 {
@@ -37,7 +25,7 @@
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%);
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) */
@@ -80,7 +68,7 @@
пересечения нитей у центра — ярче (энергетический хаб). Прозрачность слоёв — inline (×spotlight/глубину). */
.fg-plasma-flare { /* нижний: широкое насыщенное голубое плазменное свечение (по референсу) */
fill: none;
stroke: #00bfff; /* глубокий голубой */
stroke: var(--rel-shining); /* глубокий голубой */
stroke-width: 16;
stroke-linecap: round;
filter: url(#fg-plasma-blur6); /* мягкое объёмное свечение (гладкие края — как на референсе) */
@@ -90,7 +78,7 @@
}
.fg-plasma-tube { /* средний: яркая толстая неоновая трубка */
fill: none;
stroke: #00e5ff; /* яркий циан */
stroke: var(--rel-shining); /* яркий циан */
stroke-width: 6;
stroke-linecap: round;
filter: url(#fg-plasma-blur2); /* SVG feGaussianBlur stdDeviation=2 */
@@ -98,7 +86,7 @@
}
.fg-plasma-core { /* верхний: яркое чёткое ядро (светло-голубо-белое) */
fill: none;
stroke: #dffaff; /* светло-голубо-белое — «жидкое» ядро */
stroke: var(--text-primary); /* светло-голубо-белое — «жидкое» ядро */
stroke-width: 2;
stroke-linecap: round;
}
@@ -192,12 +180,12 @@
сливается со стеклом без жёсткого ободка. Силу растушёвки крутим этими двумя параметрами. */
--feather-full: 62%;
--feather-edge: 78%;
-webkit-mask-image: radial-gradient(circle at 50% 50%, #000 var(--feather-full), transparent var(--feather-edge));
mask-image: radial-gradient(circle at 50% 50%, #000 var(--feather-full), transparent var(--feather-edge));
-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: #454b55; color: #ffffff; font-weight: 600; font-size: 20px;
background: var(--surface); color: var(--text-primary); font-weight: 600; font-size: 20px;
}
@@ -226,30 +214,30 @@
}
.fg-node.is-family .node-dot {
background: linear-gradient(165deg, #785038, #5f3e2c);
border-color: rgba(255, 194, 143, 0.6);
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, #2f4f80, #2a3f62);
border-color: rgba(150, 190, 255, 0.5);
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, #4a3b7a, #2f2750);
border-color: rgba(196, 165, 255, 0.55);
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, #36435c, #283142);
border-color: rgba(180, 200, 226, 0.4);
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, #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);
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), а неоновое кольцо
@@ -258,8 +246,8 @@
.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);
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) {
@@ -269,11 +257,11 @@
/* «Сияние» на аватарке теперь стационарное. Интенсивность зафиксирована примерно на 2/3 пути
от прежнего минимального состояния к максимальному: заметно, но без постоянного «дыхания». */
.fg-node.is-shine .node-dot {
border-color: rgba(150, 240, 255, 0.62);
border-color: color-mix(in srgb, var(--rel-shining) 62%, transparent);
box-shadow:
0 0 8.3px rgba(146, 243, 255, 0.57),
0 0 18.3px rgba(121, 235, 255, 0.39),
0 0 33px rgba(100, 220, 255, 0.24);
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. */
@@ -282,7 +270,7 @@
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%);
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;
@@ -296,7 +284,7 @@
position: absolute;
inset: -12px;
border-radius: 50%;
background: radial-gradient(circle, rgba(130, 235, 255, 0.32) 0%, rgba(130, 235, 255, 0) 70%);
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;
}
@@ -324,9 +312,10 @@
transform: translateX(-50%);
margin-top: 5px;
max-width: 110px;
font-size: 10px;
font-size: var(--text-xs, 12px);
font-weight: var(--weight-medium);
line-height: 1.1;
color: #d6e2ff;
color: var(--text-primary);
text-align: center;
white-space: nowrap;
overflow: hidden;
@@ -344,10 +333,10 @@
margin-top: 7px;
padding: 3px 10px;
border-radius: 9px;
background: rgba(8, 14, 24, 0.74);
background: var(--surface);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
color: #f4f8ff;
color: var(--text-primary);
font-weight: 600;
text-shadow: none;
}
@@ -357,26 +346,26 @@
width: 15px;
height: 15px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.25);
background: #36435c;
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: #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-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: rgba(170, 200, 240, 0.65);
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: 9px;
font-size: 11px;
opacity: 0.9;
top: calc(100% + 1px);
}
@@ -386,20 +375,20 @@
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);
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, #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);
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 rgba(150, 220, 255, 0.45), 0 0 7px rgba(115, 200, 255, 0.25); filter: brightness(0.78); }
50% { box-shadow: 0 0 7px rgba(165, 235, 255, 0.95), 0 0 15px rgba(120, 210, 255, 0.6); filter: brightness(1.3); }
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) {
@@ -434,44 +423,13 @@
}
/* Стеклянные табы — тонкие пластины матового стекла (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);
}
/* Служебные переключатели карты: история и X2 намеренно чуть компактнее основных фильтров. */
.fg-history-chip {
padding-left: 11px;
padding-right: 11px;
}
.fg-x2-chip {
min-width: 36px;
padding-left: 9px;
padding-right: 9px;
font-weight: 750;
letter-spacing: 0.02em;
}
/* Контекстное меню узла (долгое нажатие) — в #modal-root, поверх всего, не масштабируется */
.fg-menu-overlay {
@@ -486,8 +444,8 @@
padding: 8px;
display: grid;
gap: 3px;
background: rgba(16, 24, 40, 0.97);
border: 1px solid rgba(166, 196, 245, 0.28);
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);
@@ -501,13 +459,13 @@
align-items: baseline;
gap: 10px;
padding: 4px 8px 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
border-bottom: 1px solid var(--border-subtle);
margin-bottom: 3px;
}
.fg-menu-login {
font-weight: 700;
color: #eaf1ff;
color: var(--text-primary);
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
@@ -516,7 +474,7 @@
.fg-menu-rel {
font-size: 11px;
color: #9fb6e0;
color: var(--rel-contact);
flex: 0 0 auto;
}
@@ -558,22 +516,22 @@
gap: 6px;
padding: 6px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.04);
border: 0.5px solid rgba(255, 255, 255, 0.12);
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 rgba(255, 255, 255, 0.08);
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: #eaf2ff;
color: var(--text-primary);
font-size: 13px;
outline: none;
}
.fg-search input::placeholder { color: #7d8aa6; }
.fg-search .fg-search-ico { color: #9fc0ff; font-size: 13px; }
.fg-search input::placeholder { color: var(--text-primary); }
.fg-search .fg-search-ico { color: var(--rel-contact); font-size: 13px; }
/* Хлебные крошки навигации (стек погружений: Иван › Нина › Ада) */
.fg-breadcrumb {
@@ -593,10 +551,10 @@
.fg-crumb {
pointer-events: auto;
border: 0;
background: rgba(16, 24, 40, 0.7);
background: var(--surface);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: #cfe0ff;
color: var(--text-primary);
font-size: 11px;
font-weight: 600;
line-height: 1;
@@ -609,11 +567,11 @@
white-space: nowrap;
}
.fg-crumb.is-last {
background: rgba(125, 215, 255, 0.18);
color: #eaf7ff;
background: color-mix(in srgb, var(--rel-shining) 18%, transparent);
color: var(--text-primary);
cursor: default;
}
.fg-crumb-sep { color: #5f7196; font-size: 11px; align-self: center; pointer-events: none; }
.fg-crumb-sep { color: var(--on-accent); font-size: 11px; align-self: center; pointer-events: none; }
/* Доступность: визуально скрытый список графа для скринридеров (sr-only, читается ассистивными технологиями) */
.fg-a11y {
@@ -632,5 +590,5 @@
/* «Общая связь» (этот человек — и твой друг тоже): золотой ободок. Значок ★ убран по запросу. */
.fg-node.is-common .node-dot {
border-color: transparent;
box-shadow: 0 0 14px rgba(255, 200, 90, 0.4);
box-shadow: 0 0 14px color-mix(in srgb, var(--rel-family) 40%, transparent);
}