Initial commit: NexusArchitect Professional Workstation Overhaul
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Merriweather:ital,wght@0,300;0,400;0,700;1,400&display=swap');
|
||||
|
||||
:root {
|
||||
--nexus-neon: #00ff99;
|
||||
--nexus-bg: #121212;
|
||||
--nexus-card: #1a1a1a;
|
||||
--nexus-text: #ffffff;
|
||||
--nexus-paper: #F9F9F9;
|
||||
--nexus-font-sans: 'Inter', sans-serif;
|
||||
--nexus-font-serif: 'Merriweather', serif;
|
||||
|
||||
/* Safe Area Insets with fallbacks */
|
||||
--safe-area-inset-top: env(safe-area-inset-top, 0px);
|
||||
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--safe-area-inset-left: env(safe-area-inset-left, 0px);
|
||||
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
||||
|
||||
/* Transitions */
|
||||
--nexus-transition: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
|
||||
.theme-light {
|
||||
--nexus-bg: var(--nexus-paper);
|
||||
--nexus-card: #ffffff;
|
||||
--nexus-text: #121212;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: var(--nexus-bg);
|
||||
color: var(--nexus-text);
|
||||
font-family: var(--nexus-font-sans);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
/* Handle Notches */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Platform Specific Tweaks */
|
||||
.platform-mobile .nexus-button {
|
||||
min-height: var(--touch-target-size);
|
||||
min-width: var(--touch-target-size);
|
||||
font-size: 1.1rem;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
.platform-desktop .nexus-button {
|
||||
min-height: 36px;
|
||||
font-size: 0.9rem;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
/* D3.js Touch Optimization */
|
||||
svg {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: #b32121;
|
||||
padding: 1rem;
|
||||
color: white;
|
||||
margin: 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 378 B |
@@ -0,0 +1,6 @@
|
||||
// This is a JavaScript module that is loaded on demand. It can export any number of
|
||||
// functions, and may import other JavaScript modules if required.
|
||||
|
||||
export function showPrompt(message) {
|
||||
return prompt(message, 'Type anything here');
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export function attachKeyboardListener(dotNetHelper) {
|
||||
const handler = (e) => {
|
||||
// Exclude inputs, textareas, etc.
|
||||
const activeNode = document.activeElement ? document.activeElement.nodeName.toLowerCase() : '';
|
||||
if (activeNode === 'input' || activeNode === 'textarea') return;
|
||||
|
||||
if (e.key === 'f' || e.key === 'F') {
|
||||
dotNetHelper.invokeMethodAsync('OnFocusKeypressed');
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handler);
|
||||
return handler;
|
||||
}
|
||||
|
||||
export function detachKeyboardListener(handler) {
|
||||
if (handler) {
|
||||
window.removeEventListener('keydown', handler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import * as d3 from 'https://esm.sh/d3@7';
|
||||
|
||||
let simulation;
|
||||
let zoomBehavior;
|
||||
let svgElement;
|
||||
|
||||
export function mount(containerId, data, dotNetHelper) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
const width = container.clientWidth || 400;
|
||||
const height = container.clientHeight || 400;
|
||||
|
||||
// Create SVG
|
||||
svgElement = d3.select(container).append("svg")
|
||||
.attr("viewBox", [0, 0, width, height])
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%");
|
||||
|
||||
// Radial gradient for Nebula effect
|
||||
const defs = svgElement.append("defs");
|
||||
const radialGradient = defs.append("radialGradient")
|
||||
.attr("id", "nebulaGlow")
|
||||
.attr("cx", "50%")
|
||||
.attr("cy", "50%")
|
||||
.attr("r", "50%");
|
||||
radialGradient.append("stop").attr("offset", "0%").attr("stop-color", "var(--nexus-neon)").attr("stop-opacity", 1);
|
||||
radialGradient.append("stop").attr("offset", "100%").attr("stop-color", "var(--nexus-neon)").attr("stop-opacity", 0);
|
||||
|
||||
// Root Group for Zoom
|
||||
const rootGroup = svgElement.append("g").attr("class", "zoom-containment");
|
||||
|
||||
// Badge Element (TU JESTEŚ)
|
||||
const badge = rootGroup.append("g")
|
||||
.attr("class", "active-badge")
|
||||
.style("display", "none");
|
||||
|
||||
badge.append("rect")
|
||||
.attr("x", -35)
|
||||
.attr("y", -35)
|
||||
.attr("width", 70)
|
||||
.attr("height", 20)
|
||||
.attr("rx", 10)
|
||||
.attr("fill", "var(--nexus-neon)");
|
||||
|
||||
badge.append("text")
|
||||
.text("TU JESTEŚ")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("y", -21)
|
||||
.attr("fill", "#000")
|
||||
.attr("font-weight", "bold")
|
||||
.attr("font-size", "0.6rem");
|
||||
|
||||
// Attach Zoom Behavior
|
||||
zoomBehavior = d3.zoom()
|
||||
.scaleExtent([0.5, 4])
|
||||
.on("zoom", (e) => rootGroup.attr("transform", e.transform));
|
||||
|
||||
// Apply zoom but disable wheel interaction
|
||||
svgElement.call(zoomBehavior)
|
||||
.on("wheel.zoom", null);
|
||||
|
||||
|
||||
// Subtle Link Distance & Charge
|
||||
simulation = d3.forceSimulation(data.nodes)
|
||||
.force("link", d3.forceLink(data.links).id(d => d.id).distance(100))
|
||||
.force("charge", d3.forceManyBody().strength(-300))
|
||||
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||
.force("collide", d3.forceCollide().radius(40));
|
||||
|
||||
// Links
|
||||
const link = rootGroup.append("g")
|
||||
.selectAll("path")
|
||||
.data(data.links)
|
||||
.join("path")
|
||||
.attr("stroke", "rgba(255,255,255,0.1)")
|
||||
.attr("fill", "none")
|
||||
.attr("stroke-width", 1.5);
|
||||
|
||||
// Nodes
|
||||
const node = rootGroup.append("g")
|
||||
.selectAll("g")
|
||||
.data(data.nodes)
|
||||
.join("g")
|
||||
.style("cursor", "pointer")
|
||||
.on("click", (e, d) => {
|
||||
// Remove active state from all, add to clicked
|
||||
node.selectAll(".node-pill").classed("nexus-node-active", false);
|
||||
d3.select(e.currentTarget).select(".node-pill").classed("nexus-node-active", true);
|
||||
|
||||
// Show badge
|
||||
badge.style("display", "block").datum(d);
|
||||
|
||||
dotNetHelper.invokeMethodAsync('OnNodeClicked', d.id);
|
||||
})
|
||||
.call(drag(simulation));
|
||||
|
||||
// Outer glow for nodes
|
||||
node.append("circle")
|
||||
.attr("r", 30)
|
||||
.attr("fill", "url(#nebulaGlow)")
|
||||
.attr("opacity", d => d.id === 'root' ? 0.6 : 0.2);
|
||||
|
||||
// Pill shape
|
||||
node.append("rect")
|
||||
.attr("class", "node-pill")
|
||||
.attr("x", d => -(d.label.length * 4 + 10))
|
||||
.attr("y", -12)
|
||||
.attr("width", d => d.label.length * 8 + 20)
|
||||
.attr("height", 24)
|
||||
.attr("rx", 12)
|
||||
.attr("fill", "rgba(30, 30, 30, 0.8)")
|
||||
.attr("stroke", "rgba(255, 255, 255, 0.1)")
|
||||
.attr("stroke-width", 1);
|
||||
|
||||
// Labels
|
||||
node.append("text")
|
||||
.text(d => d.label)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("y", 4)
|
||||
.attr("fill", "#ccc")
|
||||
.attr("font-family", "var(--nexus-font-sans)")
|
||||
.attr("font-size", "0.8rem");
|
||||
|
||||
simulation.on("tick", () => {
|
||||
link.attr("d", d => {
|
||||
const dx = d.target.x - d.source.x;
|
||||
const dy = d.target.y - d.source.y;
|
||||
const dr = Math.sqrt(dx * dx + dy * dy);
|
||||
return `M${d.source.x},${d.source.y}A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;
|
||||
});
|
||||
|
||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||
|
||||
const activeData = badge.datum();
|
||||
if (activeData) {
|
||||
badge.attr("transform", `translate(${activeData.x},${activeData.y})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function drag(simulation) {
|
||||
function dragstarted(event) {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
event.subject.fx = event.subject.x;
|
||||
event.subject.fy = event.subject.y;
|
||||
}
|
||||
function dragged(event) {
|
||||
event.subject.fx = event.x;
|
||||
event.subject.fy = event.y;
|
||||
}
|
||||
function dragended(event) {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
event.subject.fx = null;
|
||||
event.subject.fy = null;
|
||||
}
|
||||
return d3.drag()
|
||||
.on("start", dragstarted)
|
||||
.on("drag", dragged)
|
||||
.on("end", dragended);
|
||||
}
|
||||
|
||||
export function unmount(containerId) {
|
||||
if (simulation) {
|
||||
simulation.stop();
|
||||
}
|
||||
const container = document.getElementById(containerId);
|
||||
if (container) {
|
||||
container.innerHTML = ''; // clear svg
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollToNode(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
|
||||
export function pause() {
|
||||
if (simulation) {
|
||||
simulation.stop();
|
||||
}
|
||||
}
|
||||
|
||||
export function zoomIn() {
|
||||
if (svgElement && zoomBehavior) {
|
||||
svgElement.transition().duration(300).call(zoomBehavior.scaleBy, 1.3);
|
||||
}
|
||||
}
|
||||
|
||||
export function zoomOut() {
|
||||
if (svgElement && zoomBehavior) {
|
||||
svgElement.transition().duration(300).call(zoomBehavior.scaleBy, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
export function zoomReset() {
|
||||
if (svgElement && zoomBehavior) {
|
||||
svgElement.transition().duration(500).call(zoomBehavior.transform, d3.zoomIdentity);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user