From 0e7fdcfdbfe35abe48dd496c60b5e6e71e2f2cc1 Mon Sep 17 00:00:00 2001 From: s0lray Date: Wed, 25 Mar 2026 23:00:21 -0400 Subject: [PATCH] Add tool details panel UI and D3 wiring (THE-104, THE-105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THE-104: Build panel UI component - New public/css/panel.css using existing design tokens - Fixed-position slide-in panel (340px desktop, 100vw bottom-sheet on mobile) - 250ms GPU-accelerated translateX() slide animation with will-change - All section placeholders: header, breadcrumb, badges, description, usage, OPSEC, rating, report link, CTA button - Dark mode via body.light-mode CSS variable overrides - Graceful empty states (sections hidden via .empty class) THE-105: Wire panel to D3 tree interactions - Leaf-node clicks (nodes with url, no children) open panel instead of navigating - parseName() for clean title + badge rendering - Breadcrumb built by walking parent chain to root - Selected-node ring via _panelSelected flag + stroke style in nodeUpdate - Close triggers: ✕ button, Escape key, overlay click, canvas click, same-node re-click - Clicking another leaf swaps panel content in-place - Session hash via crypto.randomUUID() stored in sessionStorage Co-Authored-By: Paperclip --- public/css/panel.css | 255 +++++++++++++++++++++++++++++++++++++++++++ public/index.html | 43 ++++++++ public/js/arf.js | 174 ++++++++++++++++++++++++++++- 3 files changed, 468 insertions(+), 4 deletions(-) create mode 100644 public/css/panel.css diff --git a/public/css/panel.css b/public/css/panel.css new file mode 100644 index 0000000..3d4470e --- /dev/null +++ b/public/css/panel.css @@ -0,0 +1,255 @@ +/* === Tool Details Panel === */ + +/* Overlay backdrop (mobile) */ +#panel-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 199; +} + +#panel-overlay.visible { + display: block; +} + +/* Panel container */ +#tool-panel { + position: fixed; + top: 0; + right: 0; + width: 340px; + height: 100vh; + overflow-y: auto; + background: var(--color-surface); + border-left: 1px solid var(--color-border); + box-shadow: var(--shadow-md); + z-index: 200; + display: flex; + flex-direction: column; + transform: translateX(100%); + transition: transform 250ms ease; + will-change: transform; +} + +#tool-panel.open { + transform: translateX(0); +} + +/* Header bar */ +#panel-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-3); + padding: var(--space-4); + border-bottom: 1px solid var(--color-border); + flex-shrink: 0; +} + +#panel-title { + font-size: var(--font-size-lg); + font-weight: bold; + color: var(--color-text-primary); + line-height: 1.3; + word-break: break-word; +} + +#panel-close { + background: none; + border: none; + color: var(--color-text-secondary); + font-size: 20px; + line-height: 1; + cursor: pointer; + padding: 2px var(--space-1); + border-radius: var(--radius-sm); + flex-shrink: 0; +} + +#panel-close:hover { + color: var(--color-text-primary); + background: var(--color-bg); +} + +/* Panel body sections */ +#panel-body { + flex: 1; + padding: var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-4); + overflow-y: auto; +} + +/* Breadcrumb */ +#panel-breadcrumb { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + line-height: 1.5; +} + +#panel-breadcrumb.empty { + display: none; +} + +/* Badges */ +#panel-badges { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); +} + +#panel-badges.empty { + display: none; +} + +/* Description */ +#panel-description-section { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +#panel-description-section.empty { + display: none; +} + +.panel-section-label { + font-size: var(--font-size-sm); + font-weight: bold; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +#panel-description { + font-size: var(--font-size-base); + color: var(--color-text-primary); + line-height: 1.5; +} + +/* Usage context */ +#panel-usage-section { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +#panel-usage-section.empty { + display: none; +} + +#panel-usage { + font-size: var(--font-size-base); + color: var(--color-text-primary); + line-height: 1.5; +} + +/* OPSEC notes */ +#panel-opsec-section { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +#panel-opsec-section.empty { + display: none; +} + +#panel-opsec { + font-size: var(--font-size-base); + color: var(--color-text-primary); + line-height: 1.5; + border-left: 3px solid var(--badge-T); + padding-left: var(--space-3); +} + +/* Rating */ +#panel-rating-section { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +#panel-rating-section.empty { + display: none; +} + +#panel-rating { + font-size: var(--font-size-base); + color: var(--color-text-primary); +} + +/* CTA button */ +#panel-cta-section { + margin-top: auto; + padding-top: var(--space-4); + border-top: 1px solid var(--color-border); + flex-shrink: 0; +} + +#panel-cta-section.empty { + display: none; +} + +#panel-open-tool { + display: block; + width: 100%; + padding: var(--space-3) var(--space-4); + background: var(--color-accent); + color: #fff; + border: none; + border-radius: var(--radius-md); + font-size: var(--font-size-base); + font-family: var(--font-family); + font-weight: bold; + cursor: pointer; + text-align: center; + text-decoration: none; +} + +#panel-open-tool:hover { + opacity: 0.88; +} + +/* Report link */ +#panel-report-section { + text-align: center; +} + +#panel-report-link { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + text-decoration: none; +} + +#panel-report-link:hover { + color: var(--color-node-stroke); +} + +/* Node selection ring */ +.node circle.selected-ring { + stroke: var(--color-accent); + stroke-width: 3px; +} + +/* Mobile: bottom-sheet */ +@media (max-width: 768px) { + #tool-panel { + width: 100vw; + height: 80vh; + top: auto; + bottom: 0; + right: 0; + border-left: none; + border-top: 1px solid var(--color-border); + border-radius: var(--radius-md) var(--radius-md) 0 0; + transform: translateY(100%); + transition: transform 250ms ease; + } + + #tool-panel.open { + transform: translateY(0); + } +} diff --git a/public/index.html b/public/index.html index dd3dbf5..e24858a 100644 --- a/public/index.html +++ b/public/index.html @@ -7,6 +7,7 @@ + OSINT Framework @@ -34,6 +35,48 @@ + +
+ +

diff --git a/public/js/arf.js b/public/js/arf.js index d3a9f04..f7f14fc 100644 --- a/public/js/arf.js +++ b/public/js/arf.js @@ -32,6 +32,13 @@ var zoom = d3.zoom() svgEl.call(zoom); +// Close panel when clicking SVG background (not a node or link) +svgEl.on("click", function(event) { + if (event.target === svgEl.node() || event.target.tagName === "svg") { + closePanel(); + } +}); + var vis = svgEl.append("g") .attr("transform", "translate(" + margin[3] + "," + margin[0] + ")"); @@ -96,7 +103,15 @@ function update(source) { var nodeEnter = node.enter().append("g") .attr("class", "node") .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) - .on("click", function(event, d) { toggle(d); update(d); }); + .on("click", function(event, d) { + if (d.data.url && !d.children && !d._children) { + event.preventDefault(); + openPanel(d); + } else { + toggle(d); + update(d); + } + }); nodeEnter.append("circle") .attr("r", 1e-6) @@ -106,8 +121,8 @@ function update(source) { }); nodeEnter.append('a') - .attr("target", "_blank") - .attr('href', function(d) { return d.data.url; }) + .attr("target", function(d) { return d.data.url && !d.children && !d._children ? null : "_blank"; }) + .attr('href', function(d) { return d.data.url && !d.children && !d._children ? null : d.data.url; }) .append("text") .attr("x", function(d) { return d.children || d._children ? -10 : 10; }) .attr("dy", ".35em") @@ -145,7 +160,13 @@ function update(source) { if (d._highlighted) return getCSSVar("--color-accent"); return d._children ? getCSSVar("--color-node-fill-branch") : getCSSVar("--color-node-fill-leaf"); }) - .style("stroke-width", function(d) { return d._highlighted ? "2.5px" : "1.5px"; }); + .style("stroke", function(d) { + return d._panelSelected ? getCSSVar("--color-accent") : getCSSVar("--color-node-stroke"); + }) + .style("stroke-width", function(d) { + if (d._panelSelected) return "3px"; + return d._highlighted ? "2.5px" : "1.5px"; + }); nodeUpdate.select("text") .style("fill-opacity", 1) @@ -362,6 +383,151 @@ function safeUrl(url) { return /^https?:\/\//i.test(url) ? url : "#"; } +// === Tool Details Panel === + +var _panelNode = null; + +function openPanel(d) { + var panel = document.getElementById("tool-panel"); + var overlay = document.getElementById("panel-overlay"); + if (!panel) return; + + // If same node clicked again, close it + if (_panelNode === d) { + closePanel(); + return; + } + + // Clear previous selection ring + if (_panelNode) { + _panelNode._panelSelected = false; + } + + _panelNode = d; + d._panelSelected = true; + + // Generate/store session hash + if (typeof crypto !== "undefined" && crypto.randomUUID) { + var existing = sessionStorage.getItem("osint-session"); + if (!existing) { + sessionStorage.setItem("osint-session", crypto.randomUUID()); + } + } + + // Populate panel + var parsed = parseName(d.data.name); + + // Title + var titleEl = document.getElementById("panel-title"); + if (titleEl) titleEl.textContent = parsed.cleanName; + + // Breadcrumb (ancestors from root, skip root and leaf itself) + var breadcrumbEl = document.getElementById("panel-breadcrumb"); + if (breadcrumbEl) { + var crumbs = d.ancestors().reverse().slice(1, -1).map(function(a) { + return escapeHtml(a.data.name); + }); + if (crumbs.length > 0) { + breadcrumbEl.innerHTML = crumbs.join(" › "); + breadcrumbEl.classList.remove("empty"); + } else { + breadcrumbEl.textContent = ""; + breadcrumbEl.classList.add("empty"); + } + } + + // Badges + var badgesEl = document.getElementById("panel-badges"); + if (badgesEl) { + if (parsed.badges.length > 0) { + badgesEl.innerHTML = parsed.badges.map(function(b) { + return '' + b + ''; + }).join(""); + badgesEl.classList.remove("empty"); + } else { + badgesEl.innerHTML = ""; + badgesEl.classList.add("empty"); + } + } + + // Description + _setPanelSection("panel-description-section", "panel-description", d.data.description); + + // Usage context + _setPanelSection("panel-usage-section", "panel-usage", d.data.usage); + + // OPSEC notes + _setPanelSection("panel-opsec-section", "panel-opsec", d.data.opsec); + + // Rating + _setPanelSection("panel-rating-section", "panel-rating", d.data.rating); + + // CTA + var ctaSection = document.getElementById("panel-cta-section"); + var ctaLink = document.getElementById("panel-open-tool"); + if (ctaSection && ctaLink) { + var url = safeUrl(d.data.url); + if (url !== "#") { + ctaLink.href = url; + ctaSection.classList.remove("empty"); + } else { + ctaSection.classList.add("empty"); + } + } + + // Show panel and overlay + panel.classList.add("open"); + if (overlay) overlay.classList.add("visible"); + + // Re-render to show selection ring + update(root); +} + +function _setPanelSection(sectionId, fieldId, value) { + var section = document.getElementById(sectionId); + var field = document.getElementById(fieldId); + if (!section || !field) return; + if (value) { + field.textContent = value; + section.classList.remove("empty"); + } else { + field.textContent = ""; + section.classList.add("empty"); + } +} + +function closePanel() { + var panel = document.getElementById("tool-panel"); + var overlay = document.getElementById("panel-overlay"); + if (panel) panel.classList.remove("open"); + if (overlay) overlay.classList.remove("visible"); + + if (_panelNode) { + _panelNode._panelSelected = false; + _panelNode = null; + } + + // Re-render to remove selection ring + if (root) update(root); +} + +// Keyboard: Escape closes panel +document.addEventListener("keydown", function(e) { + if (e.key === "Escape") closePanel(); +}); + +// Wire close button and overlay once DOM is ready +document.addEventListener("DOMContentLoaded", function() { + var closeBtn = document.getElementById("panel-close"); + if (closeBtn) closeBtn.addEventListener("click", closePanel); + + var overlay = document.getElementById("panel-overlay"); + if (overlay) overlay.addEventListener("click", closePanel); +}); + +// Canvas click: close panel when clicking the SVG background (not a node) +// This is wired after svgEl is created (see below in the zoom setup area). + // Toggle light/dark mode and persist preference. function goDark() { var body = document.body;