feat(ui/arch): Optimize Graph Dynamics, Immersive Reader, and Core Stability (#19)

This PR introduces a major optimization of graph dynamics, immersive reading experience, and architectural stabilization.

### 🚀 Key Improvements

- **Knowledge Graph (Fix #16)**:
  - Implemented smooth D3.js transitions using the General Update Pattern.
  - Added "Neon Flash" entry animations and dynamic node dimming for better focus.
- **Immersive Reader (Fix #12)**:
  - Standardized centered layout (`max-width: 800px`) with **Merriweather** typography.
  - Optimized line-height and letter-spacing for premium readability.
- **Technical Code Blocks (Fix #20)**:
  - High-contrast dark containers for code snippets.
  - **JetBrains Mono** integration and neon-accented scrollbars.
- **Architectural Stabilization**:
  - Enforced a strict **'no async void'** policy in UI services using `Func<Task>`.
  - Resolved WASM runtime DI errors by implementing dummy service proxies for server-side dependencies.
  - Replaced generic 'Not Found' message with a branded Nexus preloader.

Fixes #7, Fixes #12, Fixes #16, Fixes #20.

Reviewed-on: #19
Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Co-committed-by: Marek Jasiński <jasins.marek@gmail.com>
This commit was merged in pull request #19.
This commit is contained in:
2026-05-08 18:16:09 +00:00
committed by Marek Jaisński
parent 775fb73fa9
commit 55cc3ae10d
38 changed files with 442 additions and 179 deletions
@@ -134,9 +134,10 @@ export function updateData(data) {
.attr("fill", "none")
.attr("stroke-width", d => d.relationType === 'Defines' ? 2 : 1)
.attr("stroke-dasharray", d => d.relationType === 'References' ? "5,5" : "0")
.call(e => e.transition().duration(500).attr("opacity", 1)),
.style("opacity", 0)
.call(enter => enter.transition().duration(500).style("opacity", 1)),
update => update,
exit => exit.remove()
exit => exit.transition().duration(500).style("opacity", 0).remove()
);
// Update Nodes
@@ -146,8 +147,9 @@ export function updateData(data) {
.join(
enter => {
const g = enter.append("g")
.attr("class", "node-group")
.attr("class", "node-group neon-flash-node")
.style("cursor", "pointer")
.style("opacity", 0)
.on("click", (e, d) => {
currentDotNetHelper.invokeMethodAsync('OnNodeClicked', d.id);
setActiveNode(d.id);
@@ -162,8 +164,7 @@ export function updateData(data) {
if (d.type === 'Rule') return '#ff4444';
return "url(#nebulaGlow)";
})
.attr("opacity", 0)
.transition().duration(1000).attr("opacity", d => d.group === 'current' ? 0.6 : 0.2);
.attr("opacity", d => d.group === 'current' ? 0.6 : 0.2);
g.append("rect")
.attr("class", "node-pill")
@@ -187,10 +188,12 @@ export function updateData(data) {
.attr("fill", d => d.type === 'Definition' ? 'var(--nexus-accent)' : '#ccc')
.attr("font-size", "0.8rem");
g.transition().duration(500).style("opacity", 1);
return g;
},
update => update,
exit => exit.remove()
update => update.classed("neon-flash-node", false),
exit => exit.transition().duration(500).style("opacity", 0).remove()
);
simulation.nodes(data.nodes);
@@ -223,7 +226,11 @@ export function setActiveNode(nodeId) {
if (!svgElement || !node) return;
const targetNode = node.filter(d => d.id === nodeId);
if (targetNode.empty()) return;
if (targetNode.empty()) {
dimNodes(null);
badge.style("display", "none");
return;
}
const d = targetNode.datum();
@@ -235,6 +242,9 @@ export function setActiveNode(nodeId) {
badge.style("display", "block").datum(d);
badge.attr("transform", `translate(${d.x},${d.y})`);
// Dim others
dimNodes(nodeId);
// Smooth transition
svgElement.transition().duration(1000).call(
zoomBehavior.transform,
@@ -242,6 +252,24 @@ export function setActiveNode(nodeId) {
);
}
export function dimNodes(activeNodeId) {
if (!node) return;
node.transition().duration(500)
.style("opacity", d => (activeNodeId === null || d.id === activeNodeId) ? 1 : 0.4);
if (link) {
link.transition().duration(500)
.style("opacity", d => {
if (activeNodeId === null) return 1;
// Check if this link is connected to the active node
const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
const targetId = typeof d.target === 'object' ? d.target.id : d.target;
return (sourceId === activeNodeId || targetId === activeNodeId) ? 1 : 0.1;
});
}
}
export function unmount(containerId) {
if (simulation) {
simulation.stop();