Связи (pixel-aquarium, 10.06): партия 3 (лаборатория) — общие связи + доступность
Вариант 2 (всё в лаборатории, реальный путь /network-view не трогаем). - Общие связи: среди друзей человека один помечен как «общий» (он и твой друг тоже) — золотой ободок + ★ (CSS .fg-node.is-common). В лаб-генерации addDeepLevels подставляет узнаваемого друга Ивана. - Доступность: визуально скрытый (sr-only) текстовый список графа .fg-a11y (центр + связи 1-го уровня) для скринридеров; обновляется в updateA11y при перестроении (role=region, aria-label). Автопроверки расширены до 19 ассертов (добавлены «общие связи ★» и sr-only список) — прогон 19/19 PASS. Бамп client.version → 1.2.150. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
557ea96be0
commit
519bce6b78
@ -1,2 +1,2 @@
|
||||
client.version=1.2.149
|
||||
server.version=1.2.133
|
||||
client.version=1.2.150
|
||||
server.version=1.2.134
|
||||
|
||||
@ -145,6 +145,12 @@
|
||||
- **Бейдж числа связей** — `.fg-node-badge` (число из `degreeById`, обновляется в `updateBadges`).
|
||||
- **Цветовые кластеры** — мягкая аура узла по типу связи (CSS `is-family/friend/business/contact`).
|
||||
|
||||
**Фишки (партия 3, лаборатория):**
|
||||
- **Общие связи** — среди друзей человека один помечен как «общий» (он и твой друг тоже): золотой
|
||||
ободок + ★ (CSS `is-common`; в лаб-генерации `addDeepLevels` подставляет узнаваемого друга Ивана).
|
||||
- **Доступность** — визуально скрытый (`sr-only`) текстовый список графа `.fg-a11y` (центр + связи
|
||||
1-го уровня) для скринридеров; обновляется в `updateA11y` при перестроении. Полезно и для реального пути.
|
||||
|
||||
**Автопроверки (`?fgtest`):** `js/pages/network/selftest.js` автозапускается в лаборатории при `?fgtest`,
|
||||
прогоняет 17 ассертов (центровка/collision/полукруг/spotlight/переключение/LOD/поиск/крошки/бейдж/выход) через
|
||||
детерминированные dev-хелперы движка `graph.debugState()` и `graph.pumpForTest()` (синхронно докручивают кадры
|
||||
|
||||
@ -149,7 +149,12 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
edgesSvg.setAttribute('class', 'fg-edges');
|
||||
const world = document.createElement('div');
|
||||
world.className = 'fg-world';
|
||||
stage.append(edgesSvg, world);
|
||||
// Доступность: визуально скрытый текстовый список графа для скринридеров (граф сам по себе им не читается).
|
||||
const a11y = document.createElement('div');
|
||||
a11y.className = 'fg-a11y';
|
||||
a11y.setAttribute('role', 'region');
|
||||
a11y.setAttribute('aria-label', 'Карта связей — список');
|
||||
stage.append(edgesSvg, world, a11y);
|
||||
ensureShineFilter(); // SVG-фильтр свечения «сияющих» (нужен до первой отрисовки узлов)
|
||||
|
||||
// Состояние камеры (панорамирование + зум)
|
||||
@ -433,6 +438,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
`is-${src.relationType || 'contact'}`,
|
||||
tier === 2 ? 'is-tier2' : '', // друг друзей (вдвое меньше, полупрозрачный)
|
||||
tier >= 2 ? 'is-secondary' : '',
|
||||
src.common ? 'is-common' : '', // «общая связь» — этот человек и твой друг тоже (золотой ободок ★)
|
||||
].filter(Boolean).join(' ');
|
||||
el.dataset.nodeId = String(src.id);
|
||||
|
||||
@ -472,6 +478,16 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
}
|
||||
}
|
||||
|
||||
// Доступность: текстовое представление графа для скринридеров (центр + связи 1-го уровня списком).
|
||||
function updateA11y() {
|
||||
const focus = nodes.find((n) => n.isFocus);
|
||||
const tier1 = nodes.filter((n) => n.tier === 1 && !n.isFocus);
|
||||
const rel = { family: 'семья', friend: 'друг', business: 'бизнес', contact: 'контакт' };
|
||||
const items = tier1.map((n) => `<li>${escapeHtml(n.name || n.login || String(n.id))}${n.shining ? ' — сияющий' : ''} (${rel[n.relationType] || 'связь'})</li>`).join('');
|
||||
a11y.innerHTML = `<p>Центр: ${escapeHtml(focus ? (focus.name || focus.login || '') : '')}. Связей 1-го уровня: ${tier1.length}.</p><ul>${items}</ul>`;
|
||||
}
|
||||
function escapeHtml(s) { return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
||||
|
||||
// Переиспользование узла при диффинге: сохраняем x/y/скорость, задаём новую роль и цель.
|
||||
function updateNodeRole(node, spec) {
|
||||
const src = spec.src;
|
||||
@ -502,7 +518,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
// обновляем классы элемента (роль/тип/свечение/уровень) — без пересоздания DOM
|
||||
node.el.className = spec.dotOnly
|
||||
? ['fg-node', 'fg-dot', tier >= 3 ? 'is-tier3' : '', src.shining ? 'is-shine' : '', `is-${src.relationType || 'contact'}`].filter(Boolean).join(' ')
|
||||
: ['fg-node', spec.isFocus ? 'is-focus' : '', src.shining ? 'is-shine' : '', `is-${src.relationType || 'contact'}`, tier === 2 ? 'is-tier2' : '', tier >= 2 ? 'is-secondary' : ''].filter(Boolean).join(' ');
|
||||
: ['fg-node', spec.isFocus ? 'is-focus' : '', src.shining ? 'is-shine' : '', `is-${src.relationType || 'contact'}`, tier === 2 ? 'is-tier2' : '', tier >= 2 ? 'is-secondary' : '', src.common ? 'is-common' : ''].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
// --- Рендер ----------------------------------------------------------------
|
||||
@ -1540,6 +1556,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
diveTargetId = null; surfacing = false; zoom = 1; // перестроение графа сбрасывает погружение и зум
|
||||
rebuildIndex(); // обновляем nodeById/hasDeep под новый набор узлов
|
||||
updateBadges(); // бейджи-счётчики связей под новый набор
|
||||
updateA11y(); // текстовый список графа для скринридеров
|
||||
layoutDeep(); // сразу ставим глубокие уровни на орбиты вокруг родителей
|
||||
renderDeepNodes(); // и показываем их (CSS-bloom их элементы не трогает)
|
||||
emitDiveChange(); // сбрасываем хлебные крошки (новый граф = верхний уровень)
|
||||
@ -1622,6 +1639,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
if (ro) ro.disconnect();
|
||||
edgesSvg.remove();
|
||||
world.remove();
|
||||
a11y.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -93,9 +93,21 @@ function addDeepLevels(model) {
|
||||
const extra = [];
|
||||
tier1.forEach((p) => {
|
||||
const k2 = 3 + Math.floor(seed01(p.id + ':2') * 3); // 3-5
|
||||
// «общий друг»: детерминированно берём ДРУГОГО друга Ивана — он окажется и в сети p (общая связь).
|
||||
const others = tier1.filter((o) => String(o.id) !== String(p.id));
|
||||
const common = others.length ? others[Math.floor(seed01(p.id + ':common') * others.length)] : null;
|
||||
for (let i = 0; i < k2; i += 1) {
|
||||
const id2 = `${p.id}__d2_${i}`;
|
||||
const ang2 = (i / k2) * Math.PI * 2 + seed01(p.id) * 0.6;
|
||||
// i===0 + есть общий → подставляем узнаваемого друга Ивана как ОБЩУЮ связь (золотой ободок ★)
|
||||
if (i === 0 && common) {
|
||||
extra.push({
|
||||
id: id2, login: id2, name: common.name || common.login,
|
||||
avatar: null, photo: common.photo || null, relationType: common.relationType || 'friend',
|
||||
strength: 0.5, shining: false, tier: 2, parentId: String(p.id), deepAngle: ang2, common: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// узлы 2-го уровня — полноценные (видимые) аватарки: детерминированное фото-лицо (pravatar) + имя
|
||||
const face2 = `https://i.pravatar.cc/120?img=${1 + Math.floor(seed01(id2 + 'p') * 70)}`;
|
||||
extra.push({
|
||||
|
||||
@ -102,6 +102,19 @@ export async function runNetworkSelfTest(graph, deepChipEl) {
|
||||
check('H1 бейдж числа связей на центре', !!fbOk, `центр: ${fb ? fb.textContent : 'нет'}`);
|
||||
}
|
||||
|
||||
// === Тест I: общие связи — есть узлы с золотым ободком ★ (общий друг) ===
|
||||
if (typeof document !== 'undefined') {
|
||||
const commonCount = document.querySelectorAll('.fg-node.is-common').length;
|
||||
check('I1 общие связи помечены (★)', commonCount >= 1, `узлов «общая связь»: ${commonCount}`);
|
||||
}
|
||||
|
||||
// === Тест J: доступность — текстовый список графа для скринридеров ===
|
||||
if (typeof document !== 'undefined') {
|
||||
const a11y = document.querySelector('.fg-a11y');
|
||||
const liCount = a11y ? a11y.querySelectorAll('li').length : 0;
|
||||
check('J1 sr-only список графа заполнен', !!a11y && liCount >= 1, `пунктов списка: ${liCount}`);
|
||||
}
|
||||
|
||||
// === Тест E: выход — ВСЕ узлы возвращают 100% яркости, зум отъезжает ===
|
||||
graph.exitDive();
|
||||
graph.pumpForTest();
|
||||
|
||||
@ -639,3 +639,35 @@
|
||||
cursor: default;
|
||||
}
|
||||
.fg-crumb-sep { color: #5f7196; font-size: 11px; align-self: center; pointer-events: none; }
|
||||
|
||||
/* Доступность: визуально скрытый список графа для скринридеров (sr-only, читается ассистивными технологиями) */
|
||||
.fg-a11y {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* «Общая связь» (этот человек — и твой друг тоже): золотой ободок + ★ под аватаркой */
|
||||
.fg-node.is-common .node-dot {
|
||||
border-color: rgba(255, 214, 120, 0.95);
|
||||
box-shadow: 0 0 14px rgba(255, 200, 90, 0.4);
|
||||
}
|
||||
.fg-node.is-common .node-dot::after {
|
||||
content: '★';
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
color: #ffd678;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user