diff --git a/public/css/arf.css b/public/css/arf.css
index 3238f8b..d3654f5 100644
--- a/public/css/arf.css
+++ b/public/css/arf.css
@@ -105,3 +105,75 @@ h3 {
a {
color: var(--color-node-stroke);
}
+
+/* Search */
+#search-container {
+ text-align: center;
+ padding: 8px 0 4px;
+}
+
+#search-input {
+ width: 400px;
+ max-width: 90vw;
+ 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: 4px;
+ outline: none;
+}
+
+#search-input:focus {
+ border-color: var(--color-node-stroke);
+}
+
+#search-results {
+ margin: 8px auto 0;
+ width: 600px;
+ max-width: 90vw;
+ text-align: left;
+ background: var(--color-surface);
+ border: 1px solid var(--color-link);
+ border-radius: 4px;
+ max-height: 400px;
+ overflow-y: auto;
+ display: none;
+}
+
+#search-results.visible {
+ display: block;
+}
+
+.search-result-item {
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--color-link);
+ font-size: 13px;
+}
+
+.search-result-item:last-child {
+ border-bottom: none;
+}
+
+.search-result-item a {
+ font-weight: bold;
+ color: var(--color-node-stroke);
+ text-decoration: none;
+}
+
+.search-result-item a:hover {
+ text-decoration: underline;
+}
+
+.search-result-path {
+ color: var(--color-text-secondary);
+ font-size: 11px;
+ margin-top: 2px;
+}
+
+.search-no-results {
+ padding: 12px;
+ color: var(--color-text-secondary);
+ text-align: center;
+}
diff --git a/public/index.html b/public/index.html
index e349123..f55f4eb 100644
--- a/public/index.html
+++ b/public/index.html
@@ -28,6 +28,10 @@
OSINT Framework
+
diff --git a/public/js/arf.js b/public/js/arf.js
index b3fa05f..10c5dcc 100644
--- a/public/js/arf.js
+++ b/public/js/arf.js
@@ -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]);
@@ -29,6 +30,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;
@@ -39,6 +45,7 @@ d3.json("arf.json").then(function(json) {
root.children.forEach(collapse);
update(root);
+ initSearch();
});
function update(source) {
@@ -160,6 +167,71 @@ function toggle(d) {
}
}
+// 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 = 'No results found for \u201c' + escapeHtml(query) + '\u201d
';
+ 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 '';
+ }).join("");
+
+ results.innerHTML = html;
+}
+
+function escapeHtml(str) {
+ return (str || "").replace(/&/g, "&").replace(//g, ">");
+}
+
+function escapeAttr(str) {
+ return (str || "").replace(/"/g, """).replace(/'/g, "'");
+}
+
+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;