SHA256
Переделал UI отображения связей
This commit is contained in:
@@ -4,7 +4,7 @@ import { makeProfileRoute } from '../services/shine-routes.js';
|
|||||||
import { makeProfileLinksRoute } from '../services/shine-routes.js';
|
import { makeProfileLinksRoute } from '../services/shine-routes.js';
|
||||||
import { createForceGraph } from './network/force-graph.js';
|
import { createForceGraph } from './network/force-graph.js';
|
||||||
import { engineModelFromGraphModel } from './network/adapter.js';
|
import { engineModelFromGraphModel } from './network/adapter.js';
|
||||||
import { openNodeMenu, relationLabelRu } from './network/node-menu.js';
|
import { openNodeMenu } from './network/node-menu.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'network-view', title: 'Связи' };
|
export const pageMeta = { id: 'network-view', title: 'Связи' };
|
||||||
|
|
||||||
@@ -237,7 +237,6 @@ export function render({ navigate, route, chrome } = {}) {
|
|||||||
let centerLogin = normalizeLogin(persistedCenterLogin || state.session.login || '');
|
let centerLogin = normalizeLogin(persistedCenterLogin || state.session.login || '');
|
||||||
let centerHistory = Array.isArray(persistedCenterHistory) ? [...persistedCenterHistory] : [];
|
let centerHistory = Array.isArray(persistedCenterHistory) ? [...persistedCenterHistory] : [];
|
||||||
let engine = null;
|
let engine = null;
|
||||||
let sheetEl = null;
|
|
||||||
let loadSeq = 0;
|
let loadSeq = 0;
|
||||||
|
|
||||||
// Фильтры слоёв (Фаза 3). Фокус всегда виден; предикат применяется к периферийным узлам.
|
// Фильтры слоёв (Фаза 3). Фокус всегда виден; предикат применяется к периферийным узлам.
|
||||||
@@ -400,41 +399,6 @@ export function render({ navigate, route, chrome } = {}) {
|
|||||||
window.setTimeout(() => inputEl.focus(), 0);
|
window.setTimeout(() => inputEl.focus(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Нижний сниппет (bottom sheet) с краткой сводкой об узле; не блокирует карту.
|
|
||||||
function showNodeSheet(node) {
|
|
||||||
if (!sheetEl) {
|
|
||||||
sheetEl = document.createElement('div');
|
|
||||||
sheetEl.className = 'fg-sheet';
|
|
||||||
stage.append(sheetEl);
|
|
||||||
}
|
|
||||||
const login = normalizeLogin(node.login);
|
|
||||||
const shineBadge = node.shining ? '<span class="fg-sheet-badge">сияющий</span>' : '';
|
|
||||||
sheetEl.innerHTML = `
|
|
||||||
<button class="fg-sheet-close" type="button" data-act="close" aria-label="Закрыть">✕</button>
|
|
||||||
<div class="fg-sheet-body">
|
|
||||||
<div class="fg-sheet-title">${escapeHtml(login)} ${shineBadge}</div>
|
|
||||||
<div class="fg-sheet-rel">${escapeHtml(relationLabelRu(node.relationType))}</div>
|
|
||||||
</div>
|
|
||||||
<div class="fg-sheet-actions">
|
|
||||||
<button class="ghost-btn" type="button" data-act="profile">Профиль</button>
|
|
||||||
<button class="primary-btn" type="button" data-act="write">Написать</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
sheetEl.classList.add('is-open');
|
|
||||||
sheetEl.onclick = (e) => {
|
|
||||||
const btn = e.target instanceof HTMLElement ? e.target.closest('[data-act]') : null;
|
|
||||||
if (!(btn instanceof HTMLElement)) return;
|
|
||||||
const act = btn.dataset.act;
|
|
||||||
if (act === 'close') hideNodeSheet();
|
|
||||||
else if (act === 'profile') { const r = profileInfoRoute(login); if (r) navigate(r); }
|
|
||||||
else if (act === 'write') navigate(`chat/${encodeURIComponent(login)}`);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideNodeSheet() {
|
|
||||||
if (sheetEl) sheetEl.classList.remove('is-open');
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureEngine(model) {
|
function ensureEngine(model) {
|
||||||
if (engine) {
|
if (engine) {
|
||||||
engine.setModel(model);
|
engine.setModel(model);
|
||||||
@@ -443,8 +407,8 @@ export function render({ navigate, route, chrome } = {}) {
|
|||||||
engine = createForceGraph({
|
engine = createForceGraph({
|
||||||
stage: board,
|
stage: board,
|
||||||
model,
|
model,
|
||||||
// тап по периферийному узлу — центрируем (грузим его граф) и показываем нижний сниппет
|
// тап по периферийному узлу — только центрируем и загружаем его граф; нижней карточки больше нет
|
||||||
onNodeTap: (node) => { showNodeSheet(node); void load(node.login, { pushHistory: true }); },
|
onNodeTap: (node) => { void load(node.login, { pushHistory: true }); },
|
||||||
// тап по центру — полноценный профиль
|
// тап по центру — полноценный профиль
|
||||||
onCenterTap: (node) => {
|
onCenterTap: (node) => {
|
||||||
const routeTo = profileInfoRoute(node.login);
|
const routeTo = profileInfoRoute(node.login);
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ import { buildArweaveDataUrl } from '../../services/arweave-file-service.js';
|
|||||||
const SVGNS = 'http://www.w3.org/2000/svg';
|
const SVGNS = 'http://www.w3.org/2000/svg';
|
||||||
|
|
||||||
// --- Параметры физики и анимации ---------------------------------------------
|
// --- Параметры физики и анимации ---------------------------------------------
|
||||||
const ORBIT_MIN = 150; // минимальный радиус орбиты (защитный отступ от центра), px
|
const ORBIT_FIRST_R = 100; // первый круг связей: был ~150px, теперь на треть ближе к центру
|
||||||
const ORBIT_MAX = 240; // максимальный радиус орбиты (слабая связь — дальше), px
|
const ORBIT_GAP = 87; // между соседними кругами >= 1.5 диаметра аватарки (58px * 1.5)
|
||||||
|
const ORBIT_NODE_GAP = 87; // минимальная дистанция между центрами соседних аватарок на одном круге
|
||||||
const K_RADIAL = 0.035; // очень мягкая пружина пера к орбите — узлы выходят «как резина»
|
const K_RADIAL = 0.035; // очень мягкая пружина пера к орбите — узлы выходят «как резина»
|
||||||
const K_FOCUS = 0.12; // мягкая пружина фокуса к центру
|
const K_FOCUS = 0.12; // мягкая пружина фокуса к центру
|
||||||
const CHARGE = 1400; // базовое отталкивание (на старте перестроения временно ослабляется)
|
const CHARGE = 1400; // базовое отталкивание (на старте перестроения временно ослабляется)
|
||||||
@@ -146,11 +147,57 @@ function ensureShineFilter() {
|
|||||||
document.body.appendChild(svg);
|
document.body.appendChild(svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Равномерный угол по кругу — гарантирует отсутствие угловых наложений при любом N.
|
// Равномерный угол по кругу. Небольшой постоянный сдвиг, чтобы первый узел
|
||||||
// Небольшой постоянный сдвиг, чтобы первый узел не «прилипал» к горизонтали.
|
// не «прилипал» к горизонтали.
|
||||||
function spreadAngle(index, total) {
|
function spreadAngle(index, total, phase = 0.52) {
|
||||||
if (total <= 0) return 0;
|
if (total <= 0) return 0;
|
||||||
return ((index / total) * Math.PI * 2 + 0.52) % (Math.PI * 2);
|
return ((index / total) * Math.PI * 2 + phase) % (Math.PI * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сколько аватарок гарантированно помещается на круге радиуса r, если расстояние
|
||||||
|
// между ЦЕНТРАМИ соседних аватарок должно быть не меньше ORBIT_NODE_GAP.
|
||||||
|
// Используем хорду, а не длину дуги — поэтому условие выполняется геометрически точно.
|
||||||
|
function orbitCapacity(radius) {
|
||||||
|
const r = Math.max(radius, ORBIT_NODE_GAP / 2 + 1);
|
||||||
|
const minAngle = 2 * Math.asin(Math.min(1, ORBIT_NODE_GAP / (2 * r)));
|
||||||
|
return Math.max(1, Math.floor((Math.PI * 2) / minAngle));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Детерминированная раскладка первого уровня по концентрическим кругам.
|
||||||
|
// Сначала максимально заполняем внутренний круг, затем второй, третий и т.д.
|
||||||
|
// Соседние кольца разнесены минимум на 87px, а каждое следующее чуть повёрнуто,
|
||||||
|
// чтобы линии второго/третьего круга естественно проходили под аватарками внутренних кругов.
|
||||||
|
function buildOrbitPlacements(total) {
|
||||||
|
const count = Math.max(0, Number(total) || 0);
|
||||||
|
const out = new Array(count);
|
||||||
|
let start = 0;
|
||||||
|
let ring = 0;
|
||||||
|
while (start < count) {
|
||||||
|
const radius = ORBIT_FIRST_R + ring * ORBIT_GAP;
|
||||||
|
const capacity = orbitCapacity(radius);
|
||||||
|
const ringCount = Math.min(capacity, count - start);
|
||||||
|
const phase = 0.52 + (ring % 2 ? Math.PI / Math.max(6, ringCount) : 0);
|
||||||
|
for (let i = 0; i < ringCount; i += 1) {
|
||||||
|
const angle = spreadAngle(i, ringCount, phase);
|
||||||
|
out[start + i] = { radius, angle, ring };
|
||||||
|
}
|
||||||
|
start += ringCount;
|
||||||
|
ring += 1;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOrbitTargets(peers) {
|
||||||
|
const list = Array.isArray(peers) ? peers : [];
|
||||||
|
const placements = buildOrbitPlacements(list.length);
|
||||||
|
list.forEach((n, i) => {
|
||||||
|
const p = placements[i] || { radius: ORBIT_FIRST_R, angle: spreadAngle(i, list.length), ring: 0 };
|
||||||
|
n.targetR = p.radius;
|
||||||
|
n.angle = p.angle;
|
||||||
|
n.orbitRing = p.ring;
|
||||||
|
n.tx = Math.cos(n.angle) * n.targetR;
|
||||||
|
n.ty = Math.sin(n.angle) * n.targetR;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Детерминированный «джиттер» по id (0..1) — чтобы орбита была органически неровной,
|
// Детерминированный «джиттер» по id (0..1) — чтобы орбита была органически неровной,
|
||||||
@@ -360,8 +407,9 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
}
|
}
|
||||||
const specs = [];
|
const specs = [];
|
||||||
const focusSrc = list.find((n) => String(n.id) === fId) || list[0];
|
const focusSrc = list.find((n) => String(n.id) === fId) || list[0];
|
||||||
if (focusSrc) specs.push({ src: focusSrc, id: String(focusSrc.id), isFocus: true, index: 0, total: 1, dotOnly: false });
|
const tier1Orbit = buildOrbitPlacements(tier1.length);
|
||||||
tier1.forEach((p, i) => specs.push({ src: p, id: String(p.id), isFocus: false, index: i, total: tier1.length, dotOnly: i >= MAX_FULL_NODES }));
|
if (focusSrc) specs.push({ src: focusSrc, id: String(focusSrc.id), isFocus: true, index: 0, total: 1, dotOnly: false, orbit: null });
|
||||||
|
tier1.forEach((p, i) => specs.push({ src: p, id: String(p.id), isFocus: false, index: i, total: tier1.length, dotOnly: i >= MAX_FULL_NODES, orbit: tier1Orbit[i] }));
|
||||||
// 3-й уровень рисуем точками (микрозвёзды), 2-й — маленькими аватарками
|
// 3-й уровень рисуем точками (микрозвёзды), 2-й — маленькими аватарками
|
||||||
deep.forEach((p) => specs.push({ src: p, id: String(p.id), isFocus: false, index: 0, total: 1, dotOnly: (Number(p.tier) || 2) >= 3 }));
|
deep.forEach((p) => specs.push({ src: p, id: String(p.id), isFocus: false, index: 0, total: 1, dotOnly: (Number(p.tier) || 2) >= 3 }));
|
||||||
return { focusId: fId, specs };
|
return { focusId: fId, specs };
|
||||||
@@ -370,17 +418,16 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
function buildNodes(srcModel) {
|
function buildNodes(srcModel) {
|
||||||
const { focusId: fId, specs } = computeSpecs(srcModel);
|
const { focusId: fId, specs } = computeSpecs(srcModel);
|
||||||
focusId = fId;
|
focusId = fId;
|
||||||
return specs.map((s) => makeNodeState(s.src, s.isFocus, s.index, s.total, s.dotOnly));
|
return specs.map((s) => makeNodeState(s.src, s.isFocus, s.index, s.total, s.dotOnly, s.orbit));
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeNodeState(src, isFocus, index, total, dotOnly = false) {
|
function makeNodeState(src, isFocus, index, total, dotOnly = false, orbit = null) {
|
||||||
const strength = Math.max(0, Math.min(1, Number(src.strength) || 0.5));
|
const strength = Math.max(0, Math.min(1, Number(src.strength) || 0.5));
|
||||||
const tier = Number(src.tier) || 1;
|
const tier = Number(src.tier) || 1;
|
||||||
// органическая неровность: детерминированный джиттер радиуса (±9px) и угла (±0.2 рад)
|
// Первый уровень теперь раскладывается по строгим концентрическим кругам без джиттера:
|
||||||
const jr = (hash01(src.id) - 0.5) * 18;
|
// иначе случайное смещение могло бы снова нарушить минимальный зазор между аватарками.
|
||||||
const ja = (hash01(`${src.id}~a`) - 0.5) * 0.4;
|
const targetR = isFocus ? 0 : (orbit?.radius || ORBIT_FIRST_R);
|
||||||
const targetR = isFocus ? 0 : ORBIT_MIN + (1 - strength) * (ORBIT_MAX - ORBIT_MIN) + jr;
|
const angle = isFocus ? 0 : (orbit?.angle ?? spreadAngle(index, total));
|
||||||
const angle = isFocus ? 0 : spreadAngle(index, total) + ja;
|
|
||||||
// масштаб/прозрачность по уровню глубины: 2-й — вдвое меньше и полупрозрачный, 3-й — микрозвезда.
|
// масштаб/прозрачность по уровню глубины: 2-й — вдвое меньше и полупрозрачный, 3-й — микрозвезда.
|
||||||
const scale = isFocus ? FOCUS_SCALE : (tier === 2 ? DEEP2_SCALE : (tier >= 3 ? 1 : PRIMARY_SCALE));
|
const scale = isFocus ? FOCUS_SCALE : (tier === 2 ? DEEP2_SCALE : (tier >= 3 ? 1 : PRIMARY_SCALE));
|
||||||
const op = tier === 2 ? DEEP2_OPACITY : (tier >= 3 ? DEEP3_OPACITY : 1);
|
const op = tier === 2 ? DEEP2_OPACITY : (tier >= 3 ? DEEP3_OPACITY : 1);
|
||||||
@@ -404,6 +451,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
strength,
|
strength,
|
||||||
targetR,
|
targetR,
|
||||||
angle,
|
angle,
|
||||||
|
orbitRing: orbit?.ring || 0,
|
||||||
tx,
|
tx,
|
||||||
ty,
|
ty,
|
||||||
x: tx * INTRO_FACTOR,
|
x: tx * INTRO_FACTOR,
|
||||||
@@ -567,10 +615,9 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
node.strength = strength;
|
node.strength = strength;
|
||||||
node.relationType = src.relationType;
|
node.relationType = src.relationType;
|
||||||
node.shining = Boolean(src.shining);
|
node.shining = Boolean(src.shining);
|
||||||
const jr = (hash01(src.id) - 0.5) * 18;
|
node.targetR = spec.isFocus ? 0 : (spec.orbit?.radius || ORBIT_FIRST_R);
|
||||||
const ja = (hash01(`${src.id}~a`) - 0.5) * 0.4;
|
node.angle = spec.isFocus ? 0 : (spec.orbit?.angle ?? spreadAngle(spec.index, spec.total));
|
||||||
node.targetR = spec.isFocus ? 0 : ORBIT_MIN + (1 - strength) * (ORBIT_MAX - ORBIT_MIN) + jr;
|
node.orbitRing = spec.orbit?.ring || 0;
|
||||||
node.angle = spec.isFocus ? 0 : spreadAngle(spec.index, spec.total) + ja;
|
|
||||||
node.tx = Math.cos(node.angle) * node.targetR;
|
node.tx = Math.cos(node.angle) * node.targetR;
|
||||||
node.ty = Math.sin(node.angle) * node.targetR;
|
node.ty = Math.sin(node.angle) * node.targetR;
|
||||||
node.targetScale = spec.isFocus ? FOCUS_SCALE : (tier === 2 ? DEEP2_SCALE : (tier >= 3 ? 1 : PRIMARY_SCALE));
|
node.targetScale = spec.isFocus ? FOCUS_SCALE : (tier === 2 ? DEEP2_SCALE : (tier >= 3 ? 1 : PRIMARY_SCALE));
|
||||||
@@ -990,17 +1037,12 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
target.vy = 0;
|
target.vy = 0;
|
||||||
to.set(target.id, { x: 0, y: 0, scale: FOCUS_SCALE });
|
to.set(target.id, { x: 0, y: 0, scale: FOCUS_SCALE });
|
||||||
|
|
||||||
peers.forEach((n, i) => {
|
applyOrbitTargets(peers);
|
||||||
n.targetR = ORBIT_MIN + (1 - n.strength) * (ORBIT_MAX - ORBIT_MIN);
|
peers.forEach((n) => {
|
||||||
n.angle = spreadAngle(i, peers.length);
|
|
||||||
n.vx = 0;
|
n.vx = 0;
|
||||||
n.vy = 0;
|
n.vy = 0;
|
||||||
const tx = Math.cos(n.angle) * n.targetR;
|
|
||||||
const ty = Math.sin(n.angle) * n.targetR;
|
|
||||||
n.tx = tx; // обновляем целевую точку, иначе физика после твина утянет узел назад
|
|
||||||
n.ty = ty;
|
|
||||||
const sc = n.tier >= 2 ? SECONDARY_SCALE : PRIMARY_SCALE;
|
const sc = n.tier >= 2 ? SECONDARY_SCALE : PRIMARY_SCALE;
|
||||||
to.set(n.id, { x: tx, y: ty, scale: sc });
|
to.set(n.id, { x: n.tx, y: n.ty, scale: sc });
|
||||||
});
|
});
|
||||||
|
|
||||||
tween = {
|
tween = {
|
||||||
@@ -1091,11 +1133,8 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
const focus = nodes.find((n) => n.isFocus);
|
const focus = nodes.find((n) => n.isFocus);
|
||||||
if (focus) apply(focus, 0, 0, FOCUS_SCALE, 1);
|
if (focus) apply(focus, 0, 0, FOCUS_SCALE, 1);
|
||||||
|
|
||||||
visiblePeers.forEach((n, i) => {
|
applyOrbitTargets(visiblePeers);
|
||||||
n.targetR = ORBIT_MIN + (1 - n.strength) * (ORBIT_MAX - ORBIT_MIN);
|
visiblePeers.forEach((n) => {
|
||||||
n.angle = spreadAngle(i, visiblePeers.length);
|
|
||||||
n.tx = Math.cos(n.angle) * n.targetR;
|
|
||||||
n.ty = Math.sin(n.angle) * n.targetR;
|
|
||||||
const sc = n.tier >= 2 ? SECONDARY_SCALE : PRIMARY_SCALE;
|
const sc = n.tier >= 2 ? SECONDARY_SCALE : PRIMARY_SCALE;
|
||||||
apply(n, n.tx, n.ty, sc, 1);
|
apply(n, n.tx, n.ty, sc, 1);
|
||||||
});
|
});
|
||||||
@@ -1598,7 +1637,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
node = oldNode; isNew = false;
|
node = oldNode; isNew = false;
|
||||||
} else {
|
} else {
|
||||||
if (oldNode) oldNode.el.remove(); // сменился тип (точка↔аватар) — заменяем элемент
|
if (oldNode) oldNode.el.remove(); // сменился тип (точка↔аватар) — заменяем элемент
|
||||||
node = makeNodeState(spec.src, spec.isFocus, spec.index, spec.total, spec.dotOnly);
|
node = makeNodeState(spec.src, spec.isFocus, spec.index, spec.total, spec.dotOnly, spec.orbit);
|
||||||
isNew = true;
|
isNew = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -531,79 +531,6 @@
|
|||||||
background: transparent;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Партия 2: бейдж-счётчик связей, поиск, хлебные крошки, цветовые кластеры ============ */
|
/* === Партия 2: бейдж-счётчик связей, поиск, хлебные крошки, цветовые кластеры ============ */
|
||||||
|
|
||||||
/* Бейдж числа связей — маленькая пилюля в правом-верхнем углу аватарки */
|
/* Бейдж числа связей — маленькая пилюля в правом-верхнем углу аватарки */
|
||||||
|
|||||||
Reference in New Issue
Block a user