mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(ui): show the node hover card above the Galaxy panels
The stacking context that keeps the scene labels below the Galaxy panels (K29) took the hover card of a node with it: next to Selection details the card was cut off by the panel. A hover card is only there while the pointer rests on a node, and then it is what the reader wants to read. The scene now keeps a second layer beside the canvas wrapper, as large as the scene, and the node card and the coverage card draw into it. drei still places them with the scene camera, so they stand where they stood before. The layer lies above every Galaxy panel and below the toolbar menus and the tooltips, so an open menu stays readable. Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
1 parent
17ec5b8f22
commit
011f414ce1
5 files changed
+166
-13
No files matched your search
@@ -135,6 +135,7 @@ import { GraphScene, computeCameraTarget, computeFitTarget, computeFrameTarget }
|
||||
import type { CameraTarget } from './GraphScene';
|
||||
import type { LabelBox } from './NodeLabels';
|
||||
import { NodeTooltipCard } from './NodeTooltipCard';
|
||||
import { HoverCardHtml } from './hover-layer';
|
||||
import GalaxyNavigator from './GalaxyNavigator';
|
||||
import { TraceEdgeFilter } from './TraceEdgeFilter';
|
||||
import { PathPicker, PathSteps } from './ScopePathControls';
|
||||
@@ -2919,9 +2920,9 @@ export default function GalaxyPanel(props: GalaxyPanelProps): JSX.Element {
|
||||
props.onSelectShadowNode?.(node);
|
||||
setNote(`${node.file_path ?? node.name}: coverage shadow. Detailed indexing reasons are not included in this layout.`);
|
||||
}}
|
||||
renderShadowTooltip={(node) => <Html position={[node.x, node.y, node.z]} center style={{ pointerEvents: 'none' }}>
|
||||
renderShadowTooltip={(node) => <HoverCardHtml position={[node.x, node.y, node.z]}>
|
||||
<div className="atlas-coverage-tooltip"><b>{node.file_path ?? node.name}</b><p>Coverage shadow: not fully indexed.</p></div>
|
||||
</Html>}
|
||||
</HoverCardHtml>}
|
||||
onBackgroundClick={handleBackgroundClick}
|
||||
renderTooltip={(node) => <NodeTooltipCard node={node} />}
|
||||
overlay={overlay}
|
||||
|
||||
@@ -112,6 +112,9 @@
|
||||
* Wurzel und ihre direkten Nachbarn). Ohne die Prop wie vorher.
|
||||
* 22. Neu (Handtest 2026-10-04, G1): die Huelle des Canvas bildet ihren
|
||||
* eigenen Stapelkontext (`isolation: isolate`). Siehe `CANVAS_STYLE`.
|
||||
* 23. Neu (Review zu K29): neben der Huelle steht die Ebene der Hover-Karten,
|
||||
* damit eine Karte ueber den Flaechen der Galaxie steht und nicht wie die
|
||||
* Namen darunter. Siehe hover-layer.tsx.
|
||||
*/
|
||||
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
@@ -136,6 +139,7 @@ import { FRAME_WINDOW_MS, recordFrameWindow, recordSceneFacts } from './frame-ra
|
||||
import { springStep } from '../agents/agent-motion';
|
||||
import type { GraphData, GraphNode } from './types';
|
||||
import { COVERAGE_SHADOW_COLOR, coverageShadowPositions, resolveCoverageShadowNode } from './coverage-shadow';
|
||||
import { HOVER_LAYER_STYLE, HoverLayerContext } from './hover-layer';
|
||||
import type { CoverageShadow, CoverageShadowNode } from './coverage-shadow';
|
||||
import {
|
||||
DEFAULT_DISPLAY_SETTINGS,
|
||||
@@ -1106,8 +1110,10 @@ export function GraphScene({
|
||||
* Beweislaeufe von W3 bis W9 gemessen haben.
|
||||
*/
|
||||
const frameloop = !active ? 'never' : frameCap > 0 ? 'never' : 'always';
|
||||
/* Aenderung 23: die Hover-Karten zeichnen ausserhalb der Huelle in diese Ebene. */
|
||||
const [hoverLayer, setHoverLayer] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
return (<>
|
||||
<Canvas
|
||||
{...background}
|
||||
frameloop={frameloop}
|
||||
@@ -1171,8 +1177,10 @@ export function GraphScene({
|
||||
</group>}
|
||||
|
||||
{overlay}
|
||||
{hovered && renderTooltip !== undefined && renderTooltip(hovered)}
|
||||
{resolveCoverageShadowNode(renderedShadow, hoveredShadow) && hoveredShadow && renderShadowTooltip?.(hoveredShadow)}
|
||||
<HoverLayerContext.Provider value={hoverLayer}>
|
||||
{hovered && renderTooltip !== undefined && renderTooltip(hovered)}
|
||||
{resolveCoverageShadowNode(renderedShadow, hoveredShadow) && hoveredShadow && renderShadowTooltip?.(hoveredShadow)}
|
||||
</HoverLayerContext.Provider>
|
||||
|
||||
<CameraAnimator target={sceneTarget} controlsRef={controlsRef} flat={flat} />
|
||||
<FitContainment nodes={renderedNodes} target={sceneTarget} controlsRef={controlsRef} moved={moved}
|
||||
@@ -1229,7 +1237,8 @@ export function GraphScene({
|
||||
/>
|
||||
{flat && <FlatTarget box={flatBox} controlsRef={controlsRef} />}
|
||||
</Canvas>
|
||||
);
|
||||
<div ref={setHoverLayer} data-testid="atlas-galaxy-hover-layer" style={HOVER_LAYER_STYLE} />
|
||||
</>);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import type { JSX } from 'react';
|
||||
import { Html } from '@react-three/drei';
|
||||
import { HoverCardHtml } from './hover-layer';
|
||||
import type { GraphNode } from './types';
|
||||
|
||||
/** Der Zeilenbereich, wenn das Layout einen fuehrt. */
|
||||
@@ -60,11 +60,8 @@ export function NodeTooltipCard({ node }: { node: GraphNode }): JSX.Element {
|
||||
const rows = tooltipRows(node);
|
||||
const lines = lineRangeOf(node);
|
||||
return (
|
||||
<Html
|
||||
position={[node.x, node.y + node.size * 0.7, node.z]}
|
||||
center
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
/* Review zu K29: in der Ebene der Hover-Karten, ueber den Flaechen der Galaxie (hover-layer.tsx). */
|
||||
<HoverCardHtml position={[node.x, node.y + node.size * 0.7, node.z]}>
|
||||
<div className="atlas-galaxy-card" data-testid="atlas-galaxy-card">
|
||||
<div className="atlas-galaxy-card-head">
|
||||
<span className="atlas-galaxy-card-dot" style={{ backgroundColor: node.color }} />
|
||||
@@ -92,7 +89,7 @@ export function NodeTooltipCard({ node }: { node: GraphNode }): JSX.Element {
|
||||
)}
|
||||
<p className="atlas-galaxy-card-action">{tooltipAction(node)}</p>
|
||||
</div>
|
||||
</Html>
|
||||
</HoverCardHtml>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// @vitest-environment jsdom
|
||||
/*
|
||||
* Review zu K29: seit G1 bildet die Huelle des Canvas ihren eigenen
|
||||
* Stapelkontext, damit die Namen der Szene unter den Bedienflaechen bleiben.
|
||||
* Dieselbe Regel legte die Hover-Karte eines Knotens unter jede Flaeche: neben
|
||||
* "Selection details" war sie abgeschnitten. Eine Hover-Karte ist fluechtig und
|
||||
* muss lesbar sein. Hier steht, dass sie in einer eigenen Ebene ausserhalb der
|
||||
* Huelle steht, ueber den Flaechen der Galaxie und unter ihren Menues und
|
||||
* Tooltips.
|
||||
*/
|
||||
import { act, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const html = vi.hoisted(() => ({ props: [] as Record<string, unknown>[] }));
|
||||
vi.mock('@react-three/drei', async importOriginal => ({
|
||||
...await importOriginal<typeof import('@react-three/drei')>(),
|
||||
Html: (props: Record<string, unknown> & { children?: ReactNode }) => { html.props.push(props); return <div data-testid="html">{props.children}</div>; },
|
||||
}));
|
||||
|
||||
const { GraphScene } = await import('./GraphScene');
|
||||
const { HoverLayerContext, HOVER_LAYER_Z_INDEX } = await import('./hover-layer');
|
||||
const { NodeTooltipCard } = await import('./NodeTooltipCard');
|
||||
|
||||
let host: HTMLDivElement, root: Root;
|
||||
beforeEach(() => {
|
||||
(globalThis as unknown as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
(globalThis as unknown as Record<string, unknown>).ResizeObserver ??= class { observe() {} unobserve() {} disconnect() {} };
|
||||
host = document.createElement('div'); document.body.append(host); root = createRoot(host);
|
||||
html.props = [];
|
||||
});
|
||||
afterEach(() => { act(() => root.unmount()); host.remove(); });
|
||||
|
||||
const node = { id: 1, x: 0, y: 0, z: 0, name: 'n1', label: 'Function', size: 3, color: '#abcdef', file_path: 'a.py' };
|
||||
|
||||
/** The z-index of the last rule whose selector list names `selector` exactly, across the given sheets. */
|
||||
function zIndex(selector: string, ...files: string[]): number {
|
||||
let found: number | undefined;
|
||||
for (const file of files) {
|
||||
const css = readFileSync(new URL(file, import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
for (const match of css.matchAll(/([^{}]+)\{([^}]*)\}/g)) {
|
||||
if (!match[1]!.split(',').map(item => item.trim()).includes(selector)) continue;
|
||||
const value = /z-index:\s*(\d+)/.exec(match[2]!)?.[1];
|
||||
if (value !== undefined) found = Number(value);
|
||||
}
|
||||
}
|
||||
expect(found, selector).toBeDefined();
|
||||
return found!;
|
||||
}
|
||||
const SHEETS = ['./graph-exploration.css', '../styles/terminal.css', '../why/selection-context.css'];
|
||||
|
||||
describe('K29: the hover card stands above the Galaxy panels', () => {
|
||||
it('GraphScene keeps a hover layer beside the isolated canvas wrapper, not inside it', () => {
|
||||
act(() => root.render(<GraphScene active={false} data={{ nodes: [node], edges: [], total_nodes: 1 }} highlightedIds={null} cameraTarget={null}
|
||||
showLabels onNodeClick={() => {}} renderTooltip={() => null} />));
|
||||
const wrapper = host.querySelector('canvas')!.parentElement!.parentElement!;
|
||||
const layer = host.querySelector<HTMLElement>('[data-testid="atlas-galaxy-hover-layer"]');
|
||||
expect(layer).not.toBeNull();
|
||||
expect(layer!.parentElement).toBe(host);
|
||||
expect(wrapper.contains(layer)).toBe(false);
|
||||
expect(layer!.style.position).toBe('absolute');
|
||||
expect(layer!.style.pointerEvents).toBe('none');
|
||||
expect(Number(layer!.style.zIndex)).toBe(HOVER_LAYER_Z_INDEX);
|
||||
});
|
||||
|
||||
it('lies over every Galaxy panel and under the toolbar menus and tooltips', () => {
|
||||
const panels = {
|
||||
'Selection details': zIndex('.galaxy-selection-evidence', ...SHEETS),
|
||||
'path panel': zIndex('.atlas-galaxy-path-panel', ...SHEETS),
|
||||
'hierarchy key': zIndex('.atlas-hierarchy-key', ...SHEETS),
|
||||
'fit view': zIndex('.atlas-galaxy-fit', ...SHEETS),
|
||||
'agents instrument': zIndex('.atlas-agents', ...SHEETS),
|
||||
'agents strip': zIndex('.atlas-galaxy-bottom', ...SHEETS),
|
||||
};
|
||||
const above = {
|
||||
'Path to menu': zIndex('.atlas-graph-path-menu', ...SHEETS),
|
||||
'Edge types menu': zIndex('.atlas-trace-edge-menu', ...SHEETS),
|
||||
'more menu': zIndex('.atlas-graph-limits-menu', ...SHEETS),
|
||||
'Recent menu': zIndex('.atlas-graph-recent-menu', ...SHEETS),
|
||||
'search results': zIndex('.atlas-galaxy-search-results', ...SHEETS),
|
||||
tooltip: zIndex('.atlas-hint', ...SHEETS),
|
||||
};
|
||||
for (const [name, value] of Object.entries(panels)) expect(HOVER_LAYER_Z_INDEX, name).toBeGreaterThan(value);
|
||||
for (const [name, value] of Object.entries(above)) expect(HOVER_LAYER_Z_INDEX, name).toBeLessThan(value);
|
||||
});
|
||||
|
||||
it('the node card draws into that layer', () => {
|
||||
function Probe() {
|
||||
const [layer, setLayer] = useState<HTMLDivElement | null>(null);
|
||||
return <><div ref={setLayer} /><HoverLayerContext.Provider value={layer}><NodeTooltipCard node={node} /></HoverLayerContext.Provider></>;
|
||||
}
|
||||
act(() => root.render(<Probe />));
|
||||
const props = html.props.at(-1)!;
|
||||
expect((props.portal as { current: HTMLElement }).current).toBe(host.firstElementChild);
|
||||
expect(props.center).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Die Ebene der Hover-Karten (Review zu K29).
|
||||
*
|
||||
* Seit G1 bildet die Huelle des Canvas ihren eigenen Stapelkontext: jedes
|
||||
* <Html> der Szene bleibt darin und damit unter den Bedienflaechen der
|
||||
* Galaxie. Fuer Namen ist das richtig, fuer eine Hover-Karte nicht: sie
|
||||
* erscheint nur, solange der Zeiger auf einem Knoten steht, und ist dann das,
|
||||
* was der Leser lesen will. Neben "Selection details" war sie abgeschnitten.
|
||||
*
|
||||
* Darum steht neben der Huelle eine eigene Ebene, so gross wie die Szene, und
|
||||
* die Karten haengen sich dort hinein (drei `portal`). drei rechnet ihre Lage
|
||||
* weiter mit der Kamera der Szene; die Ebene liegt deckungsgleich ueber dem
|
||||
* Canvas, also steht die Karte am selben Punkt wie vorher. Ihr z-index liegt
|
||||
* ueber jeder Flaeche der Galaxie (Selection details 4, Hierarchie-Notiz 20,
|
||||
* Pfadliste 25) und unter den Menues der Werkzeugleiste (30, die Suche 35) und
|
||||
* den Tooltips (40): ein offenes Menue bleibt lesbar, auch wenn eine Karte
|
||||
* darunter steht.
|
||||
*/
|
||||
import { createContext, useContext, useMemo } from 'react';
|
||||
import type { CSSProperties, JSX, ReactNode, RefObject } from 'react';
|
||||
import { Html } from '@react-three/drei';
|
||||
|
||||
export const HOVER_LAYER_Z_INDEX = 28;
|
||||
|
||||
export const HOVER_LAYER_STYLE: CSSProperties = {
|
||||
position: 'absolute', inset: 0, zIndex: HOVER_LAYER_Z_INDEX, pointerEvents: 'none', overflow: 'hidden',
|
||||
};
|
||||
|
||||
/*
|
||||
* Die Ebene, in die eine Hover-Karte zeichnet; ohne sie bleibt die Karte, wie
|
||||
* drei es tut, in der Huelle. Das Element selbst und kein Ref: drei liest sein
|
||||
* Ziel beim Rendern, und erst ein neuer Wert sagt der Karte, dass die Ebene
|
||||
* jetzt steht.
|
||||
*/
|
||||
export const HoverLayerContext = createContext<HTMLElement | null>(null);
|
||||
|
||||
/** Eine Karte an einem Punkt der Szene, in der Ebene der Hover-Karten. */
|
||||
export function HoverCardHtml({ position, children }: { position: [number, number, number]; children: ReactNode }): JSX.Element {
|
||||
const layer = useContext(HoverLayerContext);
|
||||
const portal = useMemo(() => (layer ? { current: layer } : undefined), [layer]);
|
||||
return (
|
||||
<Html position={position} center style={{ pointerEvents: 'none' }}
|
||||
{...(portal ? { portal: portal as RefObject<HTMLElement>, zIndexRange: [1, 0] } : {})}>
|
||||
{children}
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user