SHA256
Связи (pixel-aquarium, 10.06): партия 2 (UI-фишки) — поиск, хлебные крошки, бейджи, цветовые кластеры
Всё в лаборатории (вариант 2: реальный путь /network-view не трогаем). - Поиск + телепорт: строка .fg-search; Enter → graph.findNode(имя) → камера летит к узлу (dive в «Вселенной», иначе перецентр). - Хлебные крошки: .fg-breadcrumb «Иван › Нина › Ада» (движок шлёт onDiveChange(path), API getDivePath); клик по корню — полный сброс, по предку — навигация на его уровень. - Бейдж числа связей: .fg-node-badge (degreeById → updateBadges; у центра — число связей 1-го уровня). - Цветовые кластеры: мягкая аура узла по типу связи (CSS is-family/friend/business/contact). Автопроверки расширены до 17 ассертов (добавлены поиск/крошки/бейдж) — прогон 17/17 PASS. Фикс: TDZ breadcrumbEl (объявлен до createForceGraph, т.к. onDiveChange вызывается при монтировании). Бамп client.version → 1.2.149. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -189,6 +189,9 @@ export function renderNetworkLab({ navigate }) {
|
||||
|
||||
const model = buildLabModel(centerLogin, deepMode);
|
||||
|
||||
// Объявлено заранее: onDiveChange (вызывается уже при монтировании) ссылается на эти элементы.
|
||||
let breadcrumbEl = null; // панель хлебных крошек (создаётся ниже)
|
||||
|
||||
// Монтируем движок синхронно (не через rAF): размеры сцены движок берёт с запасом
|
||||
// (window.innerWidth) и корректирует через ResizeObserver, когда сцена попадёт в DOM.
|
||||
const graph = createForceGraph({
|
||||
@@ -212,6 +215,8 @@ export function renderNetworkLab({ navigate }) {
|
||||
// Наведение мышью / касание пальцем — ВРЕМЕННОЕ раскрытие ветки (превью): выплывает под курсором,
|
||||
// втягивается при уходе (если узел не закреплён кликом). Только в режиме «Вселенная».
|
||||
onNodeHover: (node, over) => { if (deepMode) graph.setHover(over ? node : null); },
|
||||
// Изменение пути погружения → перерисовываем хлебные крошки (Иван › Нина › Ада).
|
||||
onDiveChange: (path) => renderBreadcrumb(path),
|
||||
onCenterTap: (node) => {
|
||||
// в паутине тап по корню (Ивану) — глобальный сброс: свернуть все раскрытые ветки
|
||||
if (deepMode) { graph.collapseAll(); return; }
|
||||
@@ -256,6 +261,59 @@ export function renderNetworkLab({ navigate }) {
|
||||
filterBar.append(deepChip);
|
||||
stage.append(filterBar);
|
||||
|
||||
// --- Поиск + телепорт камеры: ввёл имя → камера летит к узлу (dive в «Вселенной», иначе перецентр) ---
|
||||
const searchWrap = document.createElement('div');
|
||||
searchWrap.className = 'fg-search';
|
||||
searchWrap.addEventListener('pointerdown', (e) => e.stopPropagation());
|
||||
const searchIco = document.createElement('span');
|
||||
searchIco.className = 'fg-search-ico';
|
||||
searchIco.textContent = '🔍';
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'search';
|
||||
searchInput.placeholder = 'Найти человека…';
|
||||
searchInput.setAttribute('aria-label', 'Поиск по имени');
|
||||
function doSearch() {
|
||||
const hit = graph.findNode(searchInput.value);
|
||||
if (!hit) return;
|
||||
if (deepMode) {
|
||||
graph.diveTo({ id: hit.id }); // камера летит и центрирует найденного
|
||||
} else {
|
||||
const from = centerLogin;
|
||||
centerLogin = hit.id;
|
||||
graph.setModel(buildLabModel(centerLogin, deepMode, from));
|
||||
if (activeFilter !== 'all') graph.setFilter(FILTERS[activeFilter].pred);
|
||||
}
|
||||
searchInput.blur();
|
||||
}
|
||||
searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSearch(); });
|
||||
searchWrap.append(searchIco, searchInput);
|
||||
stage.append(searchWrap);
|
||||
|
||||
// --- Хлебные крошки: стек погружений (Иван › Нина › Ада); клик по крошке — навигация назад ---
|
||||
breadcrumbEl = document.createElement('div');
|
||||
breadcrumbEl.className = 'fg-breadcrumb';
|
||||
breadcrumbEl.addEventListener('pointerdown', (e) => e.stopPropagation());
|
||||
stage.append(breadcrumbEl);
|
||||
// hoisted-функция: на неё ссылается onDiveChange (вызывается уже после монтирования UI)
|
||||
function renderBreadcrumb(path) {
|
||||
if (!breadcrumbEl) return;
|
||||
breadcrumbEl.innerHTML = '';
|
||||
const open = Array.isArray(path) && path.length > 1; // показываем только при активном погружении
|
||||
breadcrumbEl.classList.toggle('is-open', open);
|
||||
if (!open) return;
|
||||
path.forEach((p, i) => {
|
||||
if (i) { const sep = document.createElement('span'); sep.className = 'fg-crumb-sep'; sep.textContent = '›'; breadcrumbEl.append(sep); }
|
||||
const c = document.createElement('button');
|
||||
c.type = 'button';
|
||||
c.className = `fg-crumb${i === path.length - 1 ? ' is-last' : ''}`;
|
||||
c.textContent = p.name;
|
||||
if (i < path.length - 1) {
|
||||
c.addEventListener('click', () => { if (i === 0 || p.isFocus) graph.collapseAll(); else graph.diveTo({ id: p.id }); });
|
||||
}
|
||||
breadcrumbEl.append(c);
|
||||
});
|
||||
}
|
||||
|
||||
// Автопроверки: ТОЛЬКО при ?fgtest — экспонируем граф и прогоняем self-test (результат в консоль).
|
||||
if (typeof window !== 'undefined' && String(location.search).includes('fgtest')) {
|
||||
window.__fg = graph;
|
||||
|
||||
Reference in New Issue
Block a user