mirror of
https://github.com/momenbasel/keyFinder.git
synced 2026-07-23 21:20:52 +02:00
882e298d85
- Popup + findings dashboard rebuilt as a dark ops console: radar-sweep brand mark, ARMED/PASSIVE status LEDs, scanline texture, mono chrome, amber (action) / teal (secret) semantic colors - Popup: count-up stats, numbered watchlist, command-prompt input, typed terminal status line with live counts - Dashboard: clickable severity stat filters, LATEST ticker, severity row edges, LED badges, click-to-copy match chips, relative times, skeleton rows, staggered first paint, radar empty state, / and Esc shortcuts, prefers-reduced-motion support - Fix severity sort (critical sorted last) and ticker hidden override - Add preview/ harness (stubbed chrome API) for headless UI screenshots
136 lines
3.9 KiB
JavaScript
136 lines
3.9 KiB
JavaScript
document.addEventListener("DOMContentLoaded", init);
|
||
|
||
const REDUCE_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||
|
||
let lastKeywords = 0;
|
||
let lastFindings = 0;
|
||
let termBooted = false;
|
||
|
||
async function init() {
|
||
const versionLabel = document.getElementById("versionLabel");
|
||
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
|
||
await renderKeywords();
|
||
await renderStats();
|
||
updateTermLine(true);
|
||
termBooted = true;
|
||
document.getElementById("keywordForm").addEventListener("submit", handleAddKeyword);
|
||
}
|
||
|
||
/* eased count-up for stat numerals */
|
||
function setNumber(el, target) {
|
||
if (!el) return;
|
||
const from = parseInt(el.textContent, 10);
|
||
const start = Number.isFinite(from) ? from : 0;
|
||
if (REDUCE_MOTION || start === target) {
|
||
el.textContent = target;
|
||
return;
|
||
}
|
||
const duration = 380;
|
||
const t0 = performance.now();
|
||
(function tick(t) {
|
||
const p = Math.min(1, (t - t0) / duration);
|
||
const eased = 1 - Math.pow(1 - p, 3);
|
||
el.textContent = Math.round(start + (target - start) * eased);
|
||
if (p < 1) requestAnimationFrame(tick);
|
||
})(t0);
|
||
}
|
||
|
||
/* terminal status line — types once per popup open, instant after */
|
||
function updateTermLine(typed) {
|
||
const el = document.getElementById("termText");
|
||
if (!el) return;
|
||
const text = `kf> scan armed :: ${lastKeywords} keywords :: ${lastFindings} findings`;
|
||
if (!typed || REDUCE_MOTION) {
|
||
el.textContent = text;
|
||
return;
|
||
}
|
||
let i = 0;
|
||
(function type() {
|
||
el.textContent = text.slice(0, ++i);
|
||
if (i < text.length) setTimeout(type, 14);
|
||
})();
|
||
}
|
||
|
||
async function renderKeywords() {
|
||
const response = await chrome.runtime.sendMessage({ type: "getKeywords" });
|
||
const keywords = response.keywords || [];
|
||
const list = document.getElementById("keywordList");
|
||
list.innerHTML = "";
|
||
|
||
lastKeywords = keywords.length;
|
||
setNumber(document.getElementById("keywordCount"), keywords.length);
|
||
if (termBooted) updateTermLine(false);
|
||
|
||
if (keywords.length === 0) {
|
||
const empty = document.createElement("li");
|
||
empty.className = "empty-state";
|
||
empty.textContent = "no keywords configured";
|
||
list.appendChild(empty);
|
||
return;
|
||
}
|
||
|
||
for (const kw of keywords) {
|
||
const li = document.createElement("li");
|
||
li.className = "keyword-item";
|
||
|
||
const label = document.createElement("span");
|
||
label.className = "keyword-label";
|
||
label.textContent = kw;
|
||
|
||
const removeBtn = document.createElement("button");
|
||
removeBtn.className = "keyword-remove";
|
||
removeBtn.textContent = "×";
|
||
removeBtn.title = `Remove "${kw}"`;
|
||
removeBtn.addEventListener("click", () => handleRemoveKeyword(kw));
|
||
|
||
li.appendChild(label);
|
||
li.appendChild(removeBtn);
|
||
list.appendChild(li);
|
||
}
|
||
}
|
||
|
||
async function renderStats() {
|
||
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
|
||
const findings = response.findings || [];
|
||
lastFindings = findings.length;
|
||
setNumber(document.getElementById("findingCount"), findings.length);
|
||
if (termBooted) updateTermLine(false);
|
||
}
|
||
|
||
async function handleAddKeyword(e) {
|
||
e.preventDefault();
|
||
const input = document.getElementById("keywordInput");
|
||
const errorMsg = document.getElementById("errorMsg");
|
||
const keyword = input.value.trim();
|
||
|
||
errorMsg.hidden = true;
|
||
|
||
if (!keyword) {
|
||
showError("Keyword cannot be empty.");
|
||
return;
|
||
}
|
||
|
||
const result = await chrome.runtime.sendMessage({ type: "addKeyword", keyword });
|
||
|
||
if (!result.ok) {
|
||
showError(result.error);
|
||
return;
|
||
}
|
||
|
||
input.value = "";
|
||
input.focus();
|
||
await renderKeywords();
|
||
}
|
||
|
||
async function handleRemoveKeyword(keyword) {
|
||
await chrome.runtime.sendMessage({ type: "removeKeyword", keyword });
|
||
await renderKeywords();
|
||
}
|
||
|
||
function showError(msg) {
|
||
const errorMsg = document.getElementById("errorMsg");
|
||
errorMsg.textContent = msg;
|
||
errorMsg.hidden = false;
|
||
setTimeout(() => { errorMsg.hidden = true; }, 3000);
|
||
}
|