style(ui): refactor reader layout grid, fix focus mode layout collapse, fix SVG rendering dots, reorganize intelligence toolbar (#69)

Reorganized the reader toolbar and layout grid to improve visual consistency and layout robustness in Focus Mode. Fixed outline SVG rendering bugs that caused icons to show as solid dots.

Closes #70

---------

Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Reviewed-on: #69
Co-authored-by: Antigravity <antigravity@google.com>
Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #69.
This commit is contained in:
2026-06-05 09:51:29 +00:00
committed by Marek Jaisński
parent 081c6f7940
commit f18663426b
24 changed files with 2022 additions and 571 deletions
@@ -40,70 +40,70 @@ const getCategoryStyle = d => {
// 1. Rule (red/coral)
if (type === 'rule') {
return {
color: '#ff4646',
fill: 'rgba(255, 70, 70, 0.1)',
color: 'var(--nexus-node-rule, #ff4646)',
fill: 'var(--nexus-node-rule-bg, rgba(255, 70, 70, 0.1))',
opacity: 0.8,
glowKey: 'rule',
textColor: '#ff8b8b'
textColor: 'var(--nexus-node-rule-text, #ff8b8b)'
};
}
// 2. Definition (gold/amber)
if (type === 'definition') {
return {
color: '#ffb03a',
fill: 'rgba(255, 176, 58, 0.1)',
color: 'var(--nexus-node-definition, #ffb03a)',
fill: 'var(--nexus-node-definition-bg, rgba(255, 176, 58, 0.1))',
opacity: 0.8,
glowKey: 'definition',
textColor: '#ffd18c'
textColor: 'var(--nexus-node-definition-text, #ffd18c)'
};
}
// 3. Table (purple/magenta)
if (type === 'table') {
return {
color: '#d946ef',
fill: 'rgba(217, 70, 239, 0.1)',
color: 'var(--nexus-node-table, #d946ef)',
fill: 'var(--nexus-node-table-bg, rgba(217, 70, 239, 0.1))',
opacity: 0.8,
glowKey: 'table',
textColor: '#f5d0fe'
textColor: 'var(--nexus-node-table-text, #f5d0fe)'
};
}
// 4. Section (blue/indigo)
if (type === 'section') {
return {
color: '#3b82f6',
fill: 'rgba(59, 130, 246, 0.1)',
color: 'var(--nexus-node-section, #3b82f6)',
fill: 'var(--nexus-node-section-bg, rgba(59, 130, 246, 0.1))',
opacity: 0.8,
glowKey: 'section',
textColor: '#93c5fd'
textColor: 'var(--nexus-node-section-text, #93c5fd)'
};
}
// 5. Bridge (cyan/comparison)
if (group === 'bridge') {
return {
color: '#06b6d4',
fill: 'rgba(6, 182, 212, 0.1)',
color: 'var(--nexus-node-bridge, #06b6d4)',
fill: 'var(--nexus-node-bridge-bg, rgba(6, 182, 212, 0.1))',
opacity: 0.7,
glowKey: 'bridge',
textColor: '#67e8f9'
textColor: 'var(--nexus-node-bridge-text, #67e8f9)'
};
}
// 6. Current (active/focus landmark - neon green)
if (group === 'current') {
return {
color: 'var(--nexus-neon)',
fill: 'rgba(0, 255, 153, 0.15)',
color: 'var(--nexus-node-current, var(--nexus-neon))',
fill: 'var(--nexus-node-current-bg, rgba(0, 255, 153, 0.15))',
opacity: 0.9,
glowKey: 'current',
textColor: '#ffffff'
textColor: 'var(--nexus-node-current-text, #ffffff)'
};
}
// 7. Concept / Default (subtle cool steel blue/teal)
return {
color: '#00d2c4',
fill: 'rgba(0, 210, 196, 0.05)',
color: 'var(--nexus-node-concept, #00d2c4)',
fill: 'var(--nexus-node-concept-bg, rgba(0, 210, 196, 0.05))',
opacity: 0.4,
glowKey: 'concept',
textColor: '#e0e0e0'
textColor: 'var(--nexus-node-concept-text, #e0e0e0)'
};
};
@@ -131,16 +131,16 @@ const getNodeGlyph = d => {
function updateNodeAppearances() {
if (!node) return;
node.each(function(d) {
node.each(function (d) {
const g = d3.select(this);
const rect = g.select(".node-pill");
const text = g.select("text");
const isCurrent = getNodeGroup(d) === 'current';
const isSelected = activeNodeId && d.id === activeNodeId;
const showFull = !isMobileMode || isSelected || isCurrent;
if (showFull) {
rect.transition().duration(250)
.attr("x", -getPillWidth(d) / 2)
@@ -148,7 +148,7 @@ function updateNodeAppearances() {
.attr("height", 30)
.attr("rx", 15)
.attr("y", -15);
text.text(getDisplayLabel(d))
.attr("font-size", isCurrent || isSelected ? "0.85rem" : "0.8rem")
.attr("font-weight", isCurrent || isSelected ? "600" : "normal");
@@ -159,7 +159,7 @@ function updateNodeAppearances() {
.attr("height", 30)
.attr("rx", 15)
.attr("y", -15);
text.text(getNodeGlyph(d))
.attr("font-size", "0.9rem")
.attr("font-weight", "bold");
@@ -170,7 +170,7 @@ function updateNodeAppearances() {
export function setMobileMode(isMobile) {
isMobileMode = isMobile;
if (!simulation) return;
if (isMobile) {
simulation.force("charge", d3.forceManyBody().strength(-60));
simulation.force("link").distance(180);
@@ -187,7 +187,7 @@ export function setMobileMode(isMobile) {
simulation.force("link").distance(120);
simulation.force("collide", d3.forceCollide().radius(d => (getPillWidth(d) / 2) + 20));
}
updateNodeAppearances();
simulation.alpha(0.3).restart();
}
@@ -208,11 +208,11 @@ export function mount(containerId, data, dotNetHelper) {
.attr("viewBox", [0, 0, width, height])
.attr("width", "100%")
.attr("height", "100%")
.style("background", "radial-gradient(circle, #1a1a1a 0%, #121212 100%)");
.style("background", "var(--nexus-graph-bg, radial-gradient(circle, #1a1a1a 0%, #121212 100%))");
// Radial gradients for Nebula effects
const defs = svgElement.append("defs");
// Fallback radial gradient for legacy nebulaGlow
const radialGradient = defs.append("radialGradient")
.attr("id", "nebulaGlow")
@@ -223,13 +223,13 @@ export function mount(containerId, data, dotNetHelper) {
radialGradient.append("stop").attr("offset", "100%").attr("stop-color", "var(--nexus-neon)").attr("stop-opacity", 0);
const colors = {
'rule': '#ff4646',
'definition': '#ffb03a',
'table': '#d946ef',
'section': '#3b82f6',
'bridge': '#06b6d4',
'current': 'var(--nexus-neon)',
'concept': '#00d2c4'
'rule': 'var(--nexus-node-rule, #ff4646)',
'definition': 'var(--nexus-node-definition, #ffb03a)',
'table': 'var(--nexus-node-table, #d946ef)',
'section': 'var(--nexus-node-section, #3b82f6)',
'bridge': 'var(--nexus-node-bridge, #06b6d4)',
'current': 'var(--nexus-node-current, var(--nexus-neon))',
'concept': 'var(--nexus-node-concept, #00d2c4)'
};
Object.entries(colors).forEach(([key, color]) => {
@@ -275,7 +275,7 @@ export function mount(containerId, data, dotNetHelper) {
zoomBehavior = d3.zoom()
.scaleExtent([0.3, 4])
.on("zoom", (e) => rootGroup.attr("transform", e.transform));
svgElement.call(zoomBehavior).on("wheel.zoom", null);
// Use ResizeObserver for more reliable container size tracking
@@ -324,7 +324,7 @@ export function mount(containerId, data, dotNetHelper) {
return `translate(${d.x},${d.y})`;
});
}
if (badge && badge.style("display") !== "none") {
const activeData = badge.datum();
if (activeData) {
@@ -377,9 +377,9 @@ export function updateData(data) {
enter => enter.append("path")
.attr("stroke", d => {
if (d.type === 'Defines' || d.type === 'maps_to') return 'var(--nexus-accent, #00ffaa)';
if (d.type === 'Next' || d.type === 'relates_to') return 'rgba(255,255,255,0.2)';
if (d.type === 'Next' || d.type === 'relates_to') return 'var(--nexus-graph-link-secondary, rgba(255,255,255,0.2))';
if (d.type === 'Contains' || d.type === 'contains') return 'var(--nexus-neon)';
return 'rgba(255,255,255,0.1)';
return 'var(--nexus-graph-link-default, rgba(255,255,255,0.1))';
})
.attr("fill", "none")
.attr("stroke-width", d => (d.type === 'Defines' || d.type === 'maps_to') ? 2 : 1)
@@ -413,7 +413,7 @@ export function updateData(data) {
g.append("rect")
.attr("class", "node-pill")
.attr("fill", "rgba(20, 20, 20, 0.95)")
.attr("fill", "var(--nexus-node-pill-bg, rgba(20, 20, 20, 0.95))")
.attr("stroke", d => getCategoryStyle(d).color)
.attr("stroke-width", d => getNodeGroup(d) === 'current' ? 2 : 1.2);
@@ -424,9 +424,9 @@ export function updateData(data) {
g.append("title")
.text(d => d.description ? `${d.label}\n\n${d.description}` : d.label);
g.transition().duration(500).style("opacity", 1);
return g;
},
update => update.classed("neon-flash-node", false),
@@ -466,7 +466,7 @@ function drag(simulation) {
export function setActiveNode(nodeId) {
if (!svgElement || !node) return;
activeNodeId = nodeId;
// Safety check: ensure we only target the first occurrence if IDs are duplicated
const targetNode = node.filter(d => d.id === nodeId);
@@ -479,7 +479,7 @@ export function setActiveNode(nodeId) {
const firstMatch = targetNode.filter((d, i) => i === 0);
const d = firstMatch.datum();
if (!d || d.x === undefined || d.y === undefined || isNaN(d.x) || !isFinite(d.x) || isNaN(d.y) || !isFinite(d.y)) return;
// Reset all active classes
rootGroup.selectAll(".node-pill").classed("nexus-node-active", false);
firstMatch.select(".node-pill").classed("nexus-node-active", true);
@@ -502,7 +502,7 @@ export function setActiveNode(nodeId) {
return 20;
}));
}
updateNodeAppearances();
// Smooth transition to the first matching node
@@ -514,10 +514,10 @@ 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 => {
@@ -558,7 +558,7 @@ export function handleResize(containerId) {
svgElement.attr("viewBox", [0, 0, width, height]);
simulation.force("center", d3.forceCenter(width / 2, height / 2));
const prevMobileMode = isMobileMode;
isMobileMode = window.innerWidth < 768;
if (isMobileMode !== prevMobileMode) {