merge: resolve conflict with master — keep zoomToNode + search functions (THE-79)

Both zoomToNode() (pan/zoom, THE-79) and the search/badge additions from
master (THE-66, THE-73) are preserved. No logic changes to either side.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
s0lray
2026-03-25 00:00:56 -04:00
co-authored by Paperclip
3 changed files with 143 additions and 36 deletions
+31 -29
View File
@@ -13,6 +13,12 @@
--color-noscript-border: #7a4a00;
--color-legend: #e0e0e0;
--color-header: #e0e0e0;
/* Status badge colors — saturated enough for both light and dark backgrounds */
--badge-T: #4a9eda;
--badge-D: #e8944a;
--badge-R: #c8a030;
--badge-M: #9a7eda;
--color-accent: #4a9eda;
--color-border: #444;
@@ -147,39 +153,51 @@ a {
color: var(--color-node-stroke);
}
/* Status badge pills (used in legend) */
.badge {
display: inline-block;
font-size: 10px;
font-weight: bold;
padding: 1px 4px;
border-radius: 3px;
color: #fff;
vertical-align: middle;
}
.badge-T { background-color: var(--badge-T); }
.badge-D { background-color: var(--badge-D); }
.badge-R { background-color: var(--badge-R); }
.badge-M { background-color: var(--badge-M); }
/* Search */
#search-container {
text-align: center;
padding: var(--space-2) 0 var(--space-1);
padding: 8px 0 4px;
}
#search-input {
width: 400px;
max-width: 90vw;
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-base);
font-family: var(--font-family);
padding: 8px 12px;
font-size: 14px;
font-family: "Helvetica Neue", Helvetica;
background: var(--color-surface);
color: var(--color-text-primary);
border: 1px solid var(--color-link);
border-radius: var(--radius-sm);
border-radius: 4px;
outline: none;
}
#search-input:focus {
border-color: var(--color-accent);
box-shadow: 0 0 0 2px rgba(74, 158, 218, 0.25);
border-color: var(--color-node-stroke);
}
#search-results {
margin: var(--space-2) auto 0;
margin: 8px auto 0;
width: 600px;
max-width: 90vw;
text-align: left;
background: var(--color-surface);
border: 1px solid var(--color-link);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-md);
border-radius: 4px;
max-height: 400px;
overflow-y: auto;
display: none;
@@ -190,7 +208,7 @@ a {
}
.search-result-item {
padding: var(--space-2) var(--space-3);
padding: 8px 12px;
border-bottom: 1px solid var(--color-link);
font-size: 13px;
}
@@ -211,28 +229,12 @@ a {
.search-result-path {
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-size: 11px;
margin-top: 2px;
}
.search-no-results {
padding: var(--space-3);
padding: 12px;
color: var(--color-text-secondary);
text-align: center;
}
/* === Mobile responsive === */
@media (max-width: 768px) {
#header {
font-size: var(--font-size-lg);
top: 10px;
}
.legend {
position: relative;
width: auto;
max-width: 100%;
padding: var(--space-2) var(--space-3);
box-sizing: border-box;
}
}
+10 -4
View File
@@ -28,14 +28,20 @@
OSINT Framework
<hr/>
</div>
<div id="search-container">
<input type="text" id="search-input" placeholder="Search tools..." autocomplete="off" aria-label="Search OSINT tools">
<div id="search-results" role="listbox" aria-label="Search results"></div>
</div>
</div>
<script src="js/arf.js"></script>
<div class="legend"><p>(T) - Indicates a link to a tool that must be installed and run locally<br>
(D) - Google Dork, for more information: <a href="https://en.wikipedia.org/wiki/Google_hacking">Google Hacking</a><br>
(R) - Requires registration<br>
(M) - Indicates a URL that contains the search term and the URL itself must be edited manually<br></p>
<div class="legend"><p>
<span class="badge badge-T">T</span> - Indicates a link to a tool that must be installed and run locally<br>
<span class="badge badge-D">D</span> - Google Dork, for more information: <a href="https://en.wikipedia.org/wiki/Google_hacking">Google Hacking</a><br>
<span class="badge badge-R">R</span> - Requires registration<br>
<span class="badge badge-M">M</span> - Indicates a URL that contains the search term and the URL itself must be edited manually<br>
</p>
<button id="theme-toggle" onclick="goDark()">Switch to light mode</button>
<script>
(function() {
+102 -3
View File
@@ -3,7 +3,8 @@ var margin = [20, 120, 20, 140],
height = 800 - margin[0] - margin[2],
i = 0,
duration = 1250,
root;
root,
allSearchNodes = [];
var tree = d3.tree()
.size([height, width]);
@@ -37,6 +38,22 @@ function getCSSVar(name) {
return getComputedStyle(document.body).getPropertyValue(name).trim();
}
// Parse "(T)", "(D)", "(R)", "(M)" suffixes out of a tool name.
// Returns { cleanName: string, badges: string[] }.
var BADGE_TYPES = ['T', 'D', 'R', 'M'];
function parseName(name) {
var badges = [];
var clean = name;
BADGE_TYPES.forEach(function(b) {
var suffix = ' (' + b + ')';
if (clean.indexOf(suffix) !== -1) {
badges.push(b);
clean = clean.replace(suffix, '');
}
});
return { cleanName: clean, badges: badges };
}
d3.json("arf.json").then(function(json) {
root = d3.hierarchy(json, function(d) {
return d && d.children ? d.children.filter(function(c) { return c != null; }) : null;
@@ -44,6 +61,11 @@ d3.json("arf.json").then(function(json) {
root.x0 = height / 2;
root.y0 = 0;
// Collect all tool nodes (those with URLs) for search
allSearchNodes = root.descendants().filter(function(d) {
return d.data.url;
});
function collapse(d) {
if (d.children) {
d._children = d.children;
@@ -54,6 +76,7 @@ d3.json("arf.json").then(function(json) {
root.children.forEach(collapse);
update(root);
initSearch();
});
function update(source) {
@@ -87,11 +110,22 @@ function update(source) {
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.data.name; })
.style("fill", function(d) {
return d.data.free ? getCSSVar("--color-text-primary") : getCSSVar("--color-text-secondary");
})
.style("fill-opacity", 1e-6);
.style("fill-opacity", 1e-6)
.each(function(d) {
var parsed = parseName(d.data.name);
var el = d3.select(this);
el.append("tspan").text(parsed.cleanName);
parsed.badges.forEach(function(b) {
el.append("tspan")
.attr("dx", "4")
.style("font-size", "10px")
.style("fill", getCSSVar("--badge-" + b))
.text("(" + b + ")");
});
});
nodeEnter.append("title")
.text(function(d) {
@@ -184,6 +218,71 @@ function zoomToNode(d) {
.call(zoom.transform, d3.zoomIdentity.translate(tx, ty).scale(currentK));
}
// Client-side search over all tool nodes.
var searchDebounceTimer = null;
function initSearch() {
var input = document.getElementById("search-input");
if (!input) return;
input.addEventListener("input", function() {
clearTimeout(searchDebounceTimer);
var query = input.value.trim();
searchDebounceTimer = setTimeout(function() { doSearch(query); }, 200);
});
}
function doSearch(query) {
var results = document.getElementById("search-results");
if (!results) return;
if (!query) {
results.innerHTML = "";
results.classList.remove("visible");
return;
}
var lower = query.toLowerCase();
var matches = allSearchNodes.filter(function(d) {
var name = (d.data.name || "").toLowerCase();
var desc = (d.data.description || "").toLowerCase();
return name.indexOf(lower) !== -1 || desc.indexOf(lower) !== -1;
}).slice(0, 50);
results.classList.add("visible");
if (matches.length === 0) {
results.innerHTML = '<div class="search-no-results">No results found for \u201c' + escapeHtml(query) + '\u201d</div>';
return;
}
var html = matches.map(function(d) {
var path = d.ancestors().reverse().slice(1, -1).map(function(a) {
return escapeHtml(a.data.name);
}).join(" \u203a ");
var name = escapeHtml(d.data.name);
var url = safeUrl(d.data.url);
return '<div class="search-result-item">' +
'<a href="' + escapeAttr(url) + '" target="_blank" rel="noopener noreferrer">' + name + '</a>' +
(path ? '<div class="search-result-path">' + path + '</div>' : '') +
'</div>';
}).join("");
results.innerHTML = html;
}
function escapeHtml(str) {
return (str || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function escapeAttr(str) {
return (str || "").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
function safeUrl(url) {
if (!url) return "#";
return /^https?:\/\//i.test(url) ? url : "#";
}
// Toggle light/dark mode and persist preference.
function goDark() {
var body = document.body;