diff --git a/CHANGELOG.md b/CHANGELOG.md index f954f9e..b488690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to KeyFinder are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [SemVer](https://semver.org/spec/v2.0.0.html). +## [2.2.0] - 2026-07-18 + +### Added +- Ops-console UI redesign of the popup and findings dashboard: radar-sweep brand mark, ARMED/PASSIVE status LEDs, scanline texture, monospace UI chrome, and a semantic color system (amber = actions, teal = recovered secrets, red/orange/yellow/teal severity scale) +- Popup: count-up stat numerals, numbered watchlist entries, command-prompt keyword input with glowing focus ring, and a terminal status line typed with the live keyword/finding counts +- Dashboard: clickable severity stat tiles that filter the table, LATEST ticker for the most recent finding, `findings.log` table shell with severity-coded row edges, LED severity badges, click-to-copy match chips, relative timestamps (absolute on hover), skeleton loading rows, staggered first-paint rows, and an animated radar empty state that distinguishes "no findings yet" from "filters hide everything" +- Keyboard shortcuts on the dashboard: `/` focuses search, `Esc` clears and blurs it +- `prefers-reduced-motion` support across all animations + +### Fixed +- Severity sort treated `critical` as lowest priority (`severityOrder[s] || 5` maps `critical: 0` to the fallback), so critical findings rendered last; they now lead the table +- Latest-finding ticker no longer stays visible when storage is empty (`display: flex` was overriding the `hidden` attribute) + ## [2.1.1] - 2026-05-14 ### Added diff --git a/css/popup.css b/css/popup.css index d0db75e..d8ee0a5 100644 --- a/css/popup.css +++ b/css/popup.css @@ -1,240 +1,481 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; +/* ============================================================ + KeyFinder — ops console popup + design system: dark instrument panel · amber = action + teal = recovered secrets · mono chrome · 1px hairlines + ============================================================ */ + +:root { + --bg: #05070c; + --bg-raise: #0a0e16; + --bg-overlay: #101623; + --line: #1a2233; + --line-strong: #2a3650; + --text: #d6dde8; + --text-dim: #8b96a8; + --text-faint: #525d70; + --amber: #f5a524; + --amber-hot: #ffc45c; + --teal: #45d0c4; + --red: #ff5c5c; + --green: #3ddc97; + --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco, + Consolas, "Liberation Mono", monospace; + --ease: cubic-bezier(.24, .58, .47, .99); } +* { margin: 0; padding: 0; box-sizing: border-box; } + +html { color-scheme: dark; } + body { - width: 360px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - background: #0f0f0f; - color: #e0e0e0; - font-size: 13px; + width: 372px; + font-family: var(--mono); + background: var(--bg); + color: var(--text); + font-size: 12px; line-height: 1.5; + font-feature-settings: "tnum"; + overflow-x: hidden; } +/* vignette + scanlines — instrument-panel texture */ +body::before { + content: ""; + position: fixed; + inset: 0; + background: radial-gradient(130% 90% at 50% -10%, rgba(245,165,36,.05) 0%, transparent 45%), + radial-gradient(120% 100% at 50% 110%, rgba(0,0,0,.5) 0%, transparent 55%); + pointer-events: none; + z-index: 0; +} +body::after { + content: ""; + position: fixed; + inset: 0; + background: repeating-linear-gradient(0deg, rgba(255,255,255,.016) 0 1px, transparent 1px 3px); + pointer-events: none; + z-index: 60; +} + +::selection { background: rgba(245,165,36,.28); color: #fff; } + +:focus-visible { + outline: none; + box-shadow: 0 0 0 1px var(--bg), 0 0 0 3px var(--amber); +} + +/* ------------------------------------------------ header */ + .header { - padding: 16px 16px 12px; - border-bottom: 1px solid #1e1e1e; - background: linear-gradient(135deg, #0f0f0f 0%, #1a1a2e 100%); + position: relative; + z-index: 1; + padding: 14px 14px 12px; + border-bottom: 1px solid var(--line); + background: linear-gradient(180deg, rgba(245,165,36,.045) 0%, transparent 100%); } -.header-brand { - display: flex; - align-items: center; - gap: 8px; -} +.header-brand { display: flex; align-items: center; gap: 11px; } -.header-icon { - width: 28px; - height: 28px; +/* radar: rotating sweep ring + teal blip around the key */ +.radar { + position: relative; + flex: none; + width: 38px; + height: 38px; + border-radius: 50%; + border: 1px solid var(--line-strong); + background: radial-gradient(circle at 50% 42%, #0d1320 0%, #070a11 72%); + display: grid; + place-items: center; } +.radar::before { + content: ""; + position: absolute; + inset: -1px; + border-radius: 50%; + background: conic-gradient(from 0deg, + rgba(245,165,36,.95) 0deg, rgba(245,165,36,.22) 60deg, transparent 95deg); + -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 1.5px)); + mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 1.5px)); + animation: radar-sweep 4.2s linear infinite; +} +.radar::after { + content: ""; + position: absolute; + top: 7px; + left: 9px; + width: 3px; + height: 3px; + border-radius: 50%; + background: var(--teal); + box-shadow: 0 0 6px var(--teal); + animation: blip 2.1s ease-in-out infinite; +} +.header-icon { width: 20px; height: 20px; display: block; } + +@keyframes radar-sweep { to { transform: rotate(360deg); } } +@keyframes blip { 0%, 100% { opacity: .15; } 50% { opacity: 1; } } + +.brand-text { flex: 1; min-width: 0; } .header-brand h1 { - font-size: 18px; + font-size: 14px; font-weight: 700; - color: #ffffff; - letter-spacing: -0.3px; + color: #fff; + letter-spacing: .22em; + display: flex; + align-items: center; + gap: 7px; } .version { - font-size: 10px; + font-size: 9px; font-weight: 500; - color: #666; - background: #1a1a1a; - padding: 2px 6px; - border-radius: 4px; - margin-left: 4px; + letter-spacing: .08em; + color: var(--text-faint); + border: 1px solid var(--line); + padding: 2px 5px; + border-radius: 2px; + background: var(--bg-raise); } +.version::before { content: "["; color: var(--line-strong); } +.version::after { content: "]"; color: var(--line-strong); } .header-tagline { - margin-top: 4px; - font-size: 11px; - color: #666; + margin-top: 3px; + font-size: 9.5px; + letter-spacing: .1em; + color: var(--text-faint); + white-space: nowrap; } +.header-tagline::before { content: "// "; color: var(--amber); opacity: .7; } + +/* armed badge — pulsing status LED */ +.live-badge { + flex: none; + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 8.5px; + font-weight: 700; + letter-spacing: .2em; + color: var(--green); + border: 1px solid rgba(61,220,151,.32); + background: rgba(61,220,151,.06); + padding: 4px 7px 4px 6px; + border-radius: 2px; +} +.live-dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--green); + box-shadow: 0 0 8px var(--green); + animation: pulse 1.5s ease-in-out infinite; +} +@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } } + +/* ------------------------------------------------ stats */ .stats { + position: relative; + z-index: 1; display: flex; - gap: 12px; - padding: 12px 16px; - border-bottom: 1px solid #1e1e1e; + gap: 8px; + padding: 10px 14px; + border-bottom: 1px solid var(--line); } .stat-card { flex: 1; display: flex; flex-direction: column; - align-items: center; - padding: 10px; - background: #141414; - border-radius: 8px; - border: 1px solid #1e1e1e; + gap: 4px; + padding: 9px 11px 8px; + background: var(--bg-raise); + border: 1px solid var(--line); + border-radius: 3px; + position: relative; + overflow: hidden; +} +.stat-card::before { + content: ""; + position: absolute; + top: 0; left: 0; right: 0; + height: 1px; + background: linear-gradient(90deg, var(--accent, var(--amber)), transparent 70%); + opacity: .8; +} +.stat-findings { --accent: var(--amber); } +.stat-keywords { --accent: var(--teal); } + +.stat-top { display: flex; align-items: center; gap: 6px; } + +.stat-led { + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--accent, var(--amber)); + box-shadow: 0 0 5px var(--accent, var(--amber)); +} + +.stat-label { + font-size: 8.5px; + font-weight: 600; + color: var(--text-faint); + text-transform: uppercase; + letter-spacing: .18em; } .stat-number { font-size: 22px; font-weight: 700; + line-height: 1.1; color: #fff; + font-variant-numeric: tabular-nums; } -.stat-label { - font-size: 10px; - color: #666; - text-transform: uppercase; - letter-spacing: 0.5px; - margin-top: 2px; -} +/* ------------------------------------------------ watchlist */ -.section { - padding: 12px 16px; -} +.section { position: relative; z-index: 1; padding: 12px 14px 10px; } .section-title { - font-size: 11px; - font-weight: 600; - color: #666; + font-size: 9px; + font-weight: 700; + color: var(--text-dim); text-transform: uppercase; - letter-spacing: 0.8px; - margin-bottom: 8px; + letter-spacing: .22em; + margin-bottom: 9px; + display: flex; + align-items: baseline; + gap: 7px; +} +.section-sub { + font-size: 8.5px; + font-weight: 400; + letter-spacing: .1em; + color: var(--text-faint); + text-transform: none; } +/* command-prompt input bar */ .keyword-form { display: flex; - gap: 6px; + align-items: stretch; + background: var(--bg-raise); + border: 1px solid var(--line-strong); + border-radius: 3px; margin-bottom: 8px; + transition: border-color .15s var(--ease), box-shadow .15s var(--ease); +} +.keyword-form:focus-within { + border-color: var(--amber); + box-shadow: 0 0 0 1px rgba(245,165,36,.35), 0 0 18px rgba(245,165,36,.07); +} + +.prompt { + display: grid; + place-items: center; + padding: 0 4px 0 10px; + color: var(--amber); + font-size: 13px; + font-weight: 700; + user-select: none; } .keyword-form input { flex: 1; - padding: 7px 10px; - background: #141414; - border: 1px solid #2a2a2a; - border-radius: 6px; - color: #e0e0e0; - font-size: 12px; + min-width: 0; + padding: 8px 8px 8px 2px; + background: transparent; + border: none; + color: var(--text); + font-family: var(--mono); + font-size: 11.5px; outline: none; - transition: border-color 0.15s; -} - -.keyword-form input:focus { - border-color: #4a9eff; -} - -.keyword-form input::placeholder { - color: #444; + box-shadow: none; } +.keyword-form input::placeholder { color: var(--text-faint); } .keyword-form button { - padding: 7px 14px; - background: #4a9eff; - color: #fff; + margin: 3px; + padding: 0 13px; + background: var(--amber); + color: #120a00; border: none; - border-radius: 6px; - font-size: 12px; - font-weight: 600; + border-radius: 2px; + font-family: var(--mono); + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .12em; cursor: pointer; - transition: background 0.15s; -} - -.keyword-form button:hover { - background: #3a8eef; + transition: filter .15s var(--ease), transform .05s var(--ease); } +.keyword-form button:hover { filter: brightness(1.12); } +.keyword-form button:active { transform: translateY(1px); } .error-msg { - padding: 6px 10px; - background: #2a1515; - border: 1px solid #4a2020; - border-radius: 6px; - color: #ff6b6b; - font-size: 11px; + padding: 6px 9px; + background: rgba(255,92,92,.07); + border: 1px solid rgba(255,92,92,.3); + border-left-width: 2px; + border-radius: 2px; + color: var(--red); + font-size: 10px; + letter-spacing: .04em; margin-bottom: 8px; + animation: shake .3s var(--ease); +} +.error-msg::before { content: "ERR // "; font-weight: 700; opacity: .7; } +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-3px); } + 75% { transform: translateX(3px); } } +/* keyword list — numbered watchlist entries */ .keyword-list { list-style: none; - max-height: 240px; + counter-reset: kw; + max-height: 218px; overflow-y: auto; -} - -.keyword-list::-webkit-scrollbar { - width: 4px; -} - -.keyword-list::-webkit-scrollbar-track { - background: transparent; -} - -.keyword-list::-webkit-scrollbar-thumb { - background: #333; - border-radius: 4px; + margin: 0 -4px; + padding: 0 4px; } .keyword-item { + counter-increment: kw; display: flex; - justify-content: space-between; align-items: center; - padding: 6px 10px; - border-radius: 6px; - transition: background 0.1s; + gap: 9px; + padding: 5px 7px; + border-radius: 2px; + border-left: 2px solid transparent; + transition: background .12s var(--ease), border-color .12s var(--ease); +} +.keyword-item::before { + content: counter(kw, decimal-leading-zero); + font-size: 8.5px; + color: var(--text-faint); + letter-spacing: .05em; + flex: none; } - .keyword-item:hover { - background: #1a1a1a; + background: var(--bg-raise); + border-left-color: var(--teal); } .keyword-label { - font-family: "SF Mono", "Fira Code", "Consolas", monospace; - font-size: 12px; - color: #ccc; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 11.5px; + color: var(--text); } .keyword-remove { - width: 22px; - height: 22px; - display: flex; - align-items: center; - justify-content: center; + flex: none; + width: 20px; + height: 20px; + display: grid; + place-items: center; background: transparent; - border: none; - color: #555; - font-size: 16px; + border: 1px solid transparent; + color: var(--text-faint); + font-size: 13px; + line-height: 1; cursor: pointer; - border-radius: 4px; - transition: all 0.1s; + border-radius: 2px; + opacity: .45; + transition: all .12s var(--ease); } - +.keyword-item:hover .keyword-remove { opacity: 1; } .keyword-remove:hover { - background: #2a1515; - color: #ff6b6b; + background: rgba(255,92,92,.1); + border-color: rgba(255,92,92,.35); + color: var(--red); } .empty-state { text-align: center; - padding: 16px; - color: #444; - font-size: 12px; + padding: 14px 10px; + color: var(--text-faint); + font-size: 10.5px; + letter-spacing: .06em; + border: 1px dashed var(--line-strong); + border-radius: 3px; } +.empty-state::before { content: "[ empty watchlist ]"; display: block; margin-bottom: 2px; opacity: .6; } + +/* ------------------------------------------------ footer */ .footer { - padding: 12px 16px; - border-top: 1px solid #1e1e1e; + position: relative; + z-index: 1; + padding: 10px 14px 14px; + border-top: 1px solid var(--line); } +/* typed terminal status line */ +.term-line { + display: flex; + align-items: center; + min-height: 16px; + margin-bottom: 9px; + font-size: 9.5px; + letter-spacing: .06em; + color: var(--text-faint); + white-space: nowrap; + overflow: hidden; +} +.term-text { overflow: hidden; text-overflow: ellipsis; } +.term-cursor { + flex: none; + width: 6px; + height: 11px; + margin-left: 3px; + background: var(--amber); + animation: cursor-blink 1s steps(1) infinite; +} +@keyframes cursor-blink { 0%, 55% { opacity: 1; } 56%, 100% { opacity: 0; } } + .results-btn { - display: block; - text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; padding: 10px; - background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); - color: #4a9eff; + background: var(--amber); + color: #120a00; text-decoration: none; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - border: 1px solid #1e2d4a; - transition: all 0.15s; + border-radius: 3px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .16em; + transition: filter .15s var(--ease), transform .05s var(--ease); } +.results-btn:hover { filter: brightness(1.12); } +.results-btn:active { transform: translateY(1px); } +.btn-arrow { transition: transform .18s var(--ease); } +.results-btn:hover .btn-arrow { transform: translateX(3px); } -.results-btn:hover { - background: linear-gradient(135deg, #1e2040 0%, #1a2848 100%); - border-color: #2a4070; +/* ------------------------------------------------ scrollbars */ + +::-webkit-scrollbar { width: 5px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: #1c2536; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #2a3650; } + +/* ------------------------------------------------ reduced motion */ + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: .01ms !important; + animation-iteration-count: 1 !important; + transition-duration: .01ms !important; + } } diff --git a/css/results.css b/css/results.css index 67b6470..c8ccf37 100644 --- a/css/results.css +++ b/css/results.css @@ -1,341 +1,730 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; +/* ============================================================ + KeyFinder — findings console + dark instrument panel · amber = action · teal = secrets + severity scale: red > orange > yellow > teal + ============================================================ */ + +:root { + --bg: #05070c; + --bg-raise: #0a0e16; + --bg-overlay: #101623; + --line: #1a2233; + --line-strong: #2a3650; + --text: #d6dde8; + --text-dim: #8b96a8; + --text-faint: #525d70; + --amber: #f5a524; + --amber-hot: #ffc45c; + --teal: #45d0c4; + --red: #ff5c5c; + --orange: #ff9431; + --yellow: #ffd166; + --green: #3ddc97; + --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco, + Consolas, "Liberation Mono", monospace; + --ease: cubic-bezier(.24, .58, .47, .99); } +* { margin: 0; padding: 0; box-sizing: border-box; } + +html { color-scheme: dark; } + body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - background: #0a0a0a; - color: #e0e0e0; - font-size: 13px; + font-family: var(--mono); + background: var(--bg); + color: var(--text); + font-size: 12px; line-height: 1.5; min-height: 100vh; + font-feature-settings: "tnum"; } +/* vignette + scanlines */ +body::before { + content: ""; + position: fixed; + inset: 0; + background: radial-gradient(130% 70% at 50% -10%, rgba(245,165,36,.05) 0%, transparent 45%), + radial-gradient(120% 100% at 50% 115%, rgba(0,0,0,.45) 0%, transparent 55%); + pointer-events: none; + z-index: 0; +} +body::after { + content: ""; + position: fixed; + inset: 0; + background: repeating-linear-gradient(0deg, rgba(255,255,255,.014) 0 1px, transparent 1px 3px); + pointer-events: none; + z-index: 90; +} + +::selection { background: rgba(245,165,36,.28); color: #fff; } + +:focus-visible { + outline: none; + box-shadow: 0 0 0 1px var(--bg), 0 0 0 3px var(--amber); +} + +/* ------------------------------------------------ header */ + .header { - padding: 16px 24px; - background: #0f0f0f; - border-bottom: 1px solid #1e1e1e; + position: relative; + z-index: 1; + padding: 14px 24px 12px; + border-bottom: 1px solid var(--line); + background: linear-gradient(180deg, rgba(245,165,36,.04) 0%, transparent 100%); display: flex; flex-direction: column; - gap: 12px; + gap: 11px; } -.header-left { +.header-row { display: flex; + flex-wrap: wrap; + gap: 12px; align-items: center; - gap: 10px; + justify-content: space-between; } -.header-icon { - width: 32px; - height: 32px; -} +.header-left { display: flex; align-items: center; gap: 12px; } -.header-left h1 { - font-size: 20px; +/* radar sweep around the key */ +.radar { + position: relative; + flex: none; + width: 42px; + height: 42px; + border-radius: 50%; + border: 1px solid var(--line-strong); + background: radial-gradient(circle at 50% 42%, #0d1320 0%, #070a11 72%); + display: grid; + place-items: center; +} +.radar::before { + content: ""; + position: absolute; + inset: -1px; + border-radius: 50%; + background: conic-gradient(from 0deg, + rgba(245,165,36,.95) 0deg, rgba(245,165,36,.22) 60deg, transparent 95deg); + -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 1.5px)); + mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 1.5px)); + animation: radar-sweep 4.2s linear infinite; +} +.radar::after { + content: ""; + position: absolute; + top: 8px; + left: 10px; + width: 3px; + height: 3px; + border-radius: 50%; + background: var(--teal); + box-shadow: 0 0 6px var(--teal); + animation: blip 2.1s ease-in-out infinite; +} +.header-icon { width: 22px; height: 22px; display: block; } + +@keyframes radar-sweep { to { transform: rotate(360deg); } } +@keyframes blip { 0%, 100% { opacity: .15; } 50% { opacity: 1; } } + +.brand-text h1 { + font-size: 16px; font-weight: 700; color: #fff; + letter-spacing: .22em; + display: flex; + align-items: center; + gap: 8px; } .version { - font-size: 11px; - color: #555; - font-weight: 400; + font-size: 9px; + font-weight: 500; + letter-spacing: .08em; + color: var(--text-faint); + border: 1px solid var(--line); + padding: 2px 5px; + border-radius: 2px; + background: var(--bg-raise); } +.version::before { content: "["; color: var(--line-strong); } +.version::after { content: "]"; color: var(--line-strong); } + +.header-sub { + margin-top: 2px; + font-size: 9px; + letter-spacing: .18em; + color: var(--text-faint); + text-transform: uppercase; +} +.header-sub::before { content: "// "; color: var(--amber); opacity: .7; } + +.live-badge { + flex: none; + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 8.5px; + font-weight: 700; + letter-spacing: .2em; + color: var(--green); + border: 1px solid rgba(61,220,151,.32); + background: rgba(61,220,151,.06); + padding: 4px 8px 4px 7px; + border-radius: 2px; +} +.live-dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--green); + box-shadow: 0 0 8px var(--green); + animation: pulse 1.5s ease-in-out infinite; +} +@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } } + +/* ------------------------------------------------ header actions */ .header-actions { display: flex; flex-wrap: wrap; - gap: 10px; + gap: 8px; align-items: center; - justify-content: space-between; } +/* terminal-style search bar */ +.search-wrap { + display: flex; + align-items: center; + gap: 7px; + background: var(--bg-raise); + border: 1px solid var(--line-strong); + border-radius: 3px; + padding: 0 8px; + transition: border-color .15s var(--ease), box-shadow .15s var(--ease); +} +.search-wrap:focus-within { + border-color: var(--amber); + box-shadow: 0 0 0 1px rgba(245,165,36,.3), 0 0 18px rgba(245,165,36,.06); +} +.search-glyph { color: var(--amber); font-size: 13px; user-select: none; } +.search-wrap input { + width: 210px; + padding: 7px 0; + background: transparent; + border: none; + color: var(--text); + font-family: var(--mono); + font-size: 11.5px; + outline: none; + box-shadow: none; +} +.search-wrap input::placeholder { color: var(--text-faint); } + +.kbd { + font-family: var(--mono); + font-size: 9px; + color: var(--text-faint); + border: 1px solid var(--line-strong); + border-bottom-width: 2px; + border-radius: 3px; + padding: 1px 5px; + user-select: none; +} + +.btn-group { display: flex; gap: 6px; } + +.btn { + font-family: var(--mono); + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .1em; + padding: 7px 12px; + border-radius: 3px; + border: 1px solid transparent; + cursor: pointer; + transition: all .15s var(--ease); +} +.btn:active { transform: translateY(1px); } + +.btn-secondary { + background: transparent; + border-color: var(--line-strong); + color: var(--text-dim); +} +.btn-secondary:hover { + color: var(--amber-hot); + border-color: var(--amber); + background: rgba(245,165,36,.06); +} + +.btn-danger { + background: transparent; + border-color: rgba(255,92,92,.35); + color: var(--red); +} +.btn-danger:hover { background: rgba(255,92,92,.1); border-color: var(--red); } + +/* ------------------------------------------------ filter row */ + +.header-row-filters { gap: 10px; } + .filter-group { display: flex; gap: 8px; flex-wrap: wrap; + align-items: center; } -.filter-group select, -.filter-group input { - padding: 6px 10px; - background: #141414; - border: 1px solid #2a2a2a; - border-radius: 6px; - color: #ccc; - font-size: 12px; - outline: none; -} - -.filter-group select:focus, -.filter-group input:focus { - border-color: #4a9eff; -} - -.filter-group input { - min-width: 180px; -} - -.btn-group { +.filter { display: flex; - gap: 6px; + align-items: center; + gap: 0; + background: var(--bg-raise); + border: 1px solid var(--line); + border-radius: 3px; + overflow: hidden; + transition: border-color .15s var(--ease); +} +.filter:focus-within { border-color: var(--amber); } + +.filter-label { + font-size: 8.5px; + font-weight: 700; + letter-spacing: .16em; + text-transform: uppercase; + color: var(--amber); + background: rgba(245,165,36,.07); + border-right: 1px solid var(--line); + padding: 7px 7px 7px 9px; + user-select: none; } -.btn { - padding: 6px 14px; +.filter select { + appearance: none; + -webkit-appearance: none; + padding: 6px 24px 6px 9px; + background-color: transparent; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M1 1l3 3 3-3' stroke='%238b96a8' fill='none'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; border: none; - border-radius: 6px; - font-size: 12px; - font-weight: 600; + color: var(--text); + font-family: var(--mono); + font-size: 10.5px; + letter-spacing: .04em; + outline: none; cursor: pointer; - transition: all 0.15s; + max-width: 180px; } +.filter select option { background: var(--bg-overlay); color: var(--text); } -.btn-secondary { - background: #1a1a1a; - color: #ccc; - border: 1px solid #2a2a2a; +.meta-line { + margin-left: auto; + font-size: 9.5px; + letter-spacing: .1em; + color: var(--text-faint); + white-space: nowrap; } +.meta-line strong { color: var(--text-dim); font-weight: 600; } -.btn-secondary:hover { - background: #222; - border-color: #444; -} - -.btn-danger { - background: #2a1515; - color: #ff6b6b; - border: 1px solid #4a2020; -} - -.btn-danger:hover { - background: #3a1a1a; -} +/* ------------------------------------------------ stats bar */ .stats-bar { + position: relative; + z-index: 1; display: flex; - gap: 1px; + flex-wrap: wrap; + gap: 8px; padding: 12px 24px; - background: #0f0f0f; - border-bottom: 1px solid #1e1e1e; + border-bottom: 1px solid var(--line); } .stat-item { flex: 1; + min-width: 96px; display: flex; flex-direction: column; - align-items: center; - padding: 10px; - background: #111; - border-radius: 6px; - margin: 0 4px; + gap: 4px; + padding: 9px 12px 8px; + background: var(--bg-raise); + border: 1px solid var(--line); + border-radius: 3px; + position: relative; + overflow: hidden; + transition: border-color .15s var(--ease), background .15s var(--ease); } +.stat-item::before { + content: ""; + position: absolute; + top: 0; left: 0; right: 0; + height: 1px; + background: linear-gradient(90deg, var(--accent, var(--text-dim)), transparent 75%); +} +.stat-item[data-filter] { cursor: pointer; } +.stat-item[data-filter]:hover { background: var(--bg-overlay); border-color: var(--line-strong); } +.stat-item.active { border-color: var(--accent, var(--amber)); box-shadow: 0 0 0 1px var(--accent, var(--amber)) inset; } + +.stat-total { --accent: #d6dde8; } +.stat-critical { --accent: var(--red); } +.stat-high { --accent: var(--orange); } +.stat-medium { --accent: var(--yellow); } +.stat-low { --accent: var(--teal); } +.stat-domains { --accent: var(--text-faint); } + +.stat-top { display: flex; align-items: center; gap: 6px; } + +.stat-led { + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--accent, var(--text-dim)); + box-shadow: 0 0 5px var(--accent, var(--text-dim)); +} +.stat-critical .stat-led { animation: pulse 1.5s ease-in-out infinite; } .stat-num { - font-size: 20px; + font-size: 21px; font-weight: 700; - color: #fff; + line-height: 1.1; + color: var(--accent, #fff); + font-variant-numeric: tabular-nums; } +.stat-total .stat-num { color: #fff; } .stat-lbl { - font-size: 10px; - color: #555; + font-size: 8.5px; + font-weight: 600; + color: var(--text-faint); text-transform: uppercase; - letter-spacing: 0.5px; + letter-spacing: .18em; } -.stat-critical .stat-num { color: #ff4444; } -.stat-high .stat-num { color: #ff8c42; } -.stat-medium .stat-num { color: #ffd166; } -.stat-low .stat-num { color: #4ecdc4; } +/* ------------------------------------------------ ticker */ -.main { - padding: 16px 24px; +.ticker { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 9px; + padding: 7px 24px; + border-bottom: 1px solid var(--line); + background: rgba(245,165,36,.03); + font-size: 10px; + letter-spacing: .05em; + white-space: nowrap; + overflow: hidden; +} +.ticker[hidden] { display: none; } + +.ticker-dot { + flex: none; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--amber); + box-shadow: 0 0 6px var(--amber); + animation: pulse 1.5s ease-in-out infinite; +} +.ticker-label { + flex: none; + font-size: 8.5px; + font-weight: 700; + letter-spacing: .2em; + color: var(--amber); +} +.ticker-text { + color: var(--text-dim); + overflow: hidden; + text-overflow: ellipsis; +} +.ticker-text code { color: var(--teal); } + +/* ------------------------------------------------ table shell */ + +.main { position: relative; z-index: 1; padding: 16px 24px 20px; } + +.table-shell { + position: relative; + border: 1px solid var(--line); + border-radius: 4px; + background: rgba(10,14,22,.55); +} +/* HUD corner brackets */ +.table-shell::before, +.table-shell::after { + content: ""; + position: absolute; + width: 9px; + height: 9px; + border: 0 solid var(--amber); + opacity: .55; + pointer-events: none; +} +.table-shell::before { top: -1px; left: -1px; border-top-width: 1px; border-left-width: 1px; } +.table-shell::after { bottom: -1px; right: -1px; border-bottom-width: 1px; border-right-width: 1px; } + +.shell-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 8px 12px; + border-bottom: 1px solid var(--line); +} +.shell-title { + font-size: 9.5px; + font-weight: 700; + letter-spacing: .18em; + color: var(--text-dim); + text-transform: uppercase; +} +.shell-title::before { content: "● "; color: var(--amber); font-size: 7px; vertical-align: 1.5px; } +.shell-meta { + font-size: 9px; + letter-spacing: .1em; + color: var(--text-faint); } .findings-table { width: 100%; + min-width: 980px; border-collapse: collapse; - font-size: 12px; + font-size: 11px; } .findings-table thead th { - padding: 8px 10px; + padding: 8px 12px; text-align: left; - font-size: 10px; - font-weight: 600; - color: #555; + font-size: 8.5px; + font-weight: 700; + color: var(--text-faint); text-transform: uppercase; - letter-spacing: 0.5px; - border-bottom: 1px solid #1e1e1e; + letter-spacing: .16em; + border-bottom: 1px solid var(--line); white-space: nowrap; position: sticky; top: 0; - background: #0a0a0a; + background: var(--bg); + z-index: 2; } .findings-table tbody tr { - border-bottom: 1px solid #141414; - transition: background 0.1s; + border-bottom: 1px solid rgba(26,34,51,.5); + transition: background .1s var(--ease); } +.findings-table tbody tr:last-child { border-bottom: none; } +.findings-table tbody tr:hover { background: rgba(255,255,255,.022); } -.findings-table tbody tr:hover { - background: #111; +/* severity accent on the row's leading edge */ +.findings-table tbody tr td:first-child { box-shadow: inset 2px 0 0 transparent; } +.findings-table tbody tr.sev-critical td:first-child { box-shadow: inset 2px 0 0 var(--red); } +.findings-table tbody tr.sev-high td:first-child { box-shadow: inset 2px 0 0 var(--orange); } +.findings-table tbody tr.sev-medium td:first-child { box-shadow: inset 2px 0 0 var(--yellow); } +.findings-table tbody tr.sev-low td:first-child { box-shadow: inset 2px 0 0 var(--teal); } +.findings-table tbody tr.sev-info td:first-child { box-shadow: inset 2px 0 0 var(--text-faint); } + +/* first-paint stagger */ +.findings-table tbody.fresh tr { + opacity: 0; + animation: row-in .35s var(--ease) forwards; + animation-delay: calc(var(--i, 0) * 16ms); +} +@keyframes row-in { + from { opacity: 0; transform: translateY(3px); } + to { opacity: 1; transform: translateY(0); } } .findings-table td { - padding: 8px 10px; + padding: 7px 12px; vertical-align: middle; - max-width: 280px; + max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.td-num { color: var(--text-faint); font-size: 9.5px; } + +/* severity badge — LED + label */ .badge { - display: inline-block; - padding: 2px 8px; - border-radius: 4px; - font-size: 10px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 8px 3px 7px; + border-radius: 2px; + font-size: 8.5px; font-weight: 700; - letter-spacing: 0.3px; + letter-spacing: .14em; + color: var(--sev, var(--text-dim)); + border: 1px solid color-mix(in srgb, var(--sev, #8b96a8) 38%, transparent); + background: color-mix(in srgb, var(--sev, #8b96a8) 8%, transparent); } - -.badge-critical { - background: #3a0a0a; - color: #ff4444; - border: 1px solid #5a1515; -} - -.badge-high { - background: #3a1f0a; - color: #ff8c42; - border: 1px solid #5a3015; -} - -.badge-medium { - background: #3a3a0a; - color: #ffd166; - border: 1px solid #5a5a15; -} - -.badge-low { - background: #0a3a30; - color: #4ecdc4; - border: 1px solid #155a4a; +.badge::before { + content: ""; + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--sev, var(--text-dim)); + box-shadow: 0 0 5px var(--sev, transparent); } +.badge-critical { --sev: var(--red); } +.badge-critical::before { animation: pulse 1.5s ease-in-out infinite; } +.badge-high { --sev: var(--orange); } +.badge-medium { --sev: var(--yellow); } +.badge-low { --sev: var(--teal); } +.badge-info { --sev: var(--text-dim); } .type-badge { display: inline-block; - padding: 2px 6px; - background: #1a1a1a; - border: 1px solid #2a2a2a; - border-radius: 4px; - font-size: 10px; - color: #888; - font-family: "SF Mono", "Fira Code", monospace; + padding: 2px 7px; + background: var(--bg-raise); + border: 1px solid var(--line); + border-radius: 2px; + font-size: 9px; + letter-spacing: .06em; + color: var(--text-dim); } +/* the recovered secret — teal mono chip, click to copy */ .match-value { - font-family: "SF Mono", "Fira Code", "Consolas", monospace; - font-size: 11px; - background: #141414; - padding: 2px 6px; - border-radius: 4px; - border: 1px solid #1e1e1e; - color: #4ecdc4; - max-width: 260px; + font-family: var(--mono); + font-size: 10.5px; + background: rgba(69,208,196,.06); + padding: 3px 8px; + border-radius: 2px; + border: 1px solid rgba(69,208,196,.22); + color: var(--teal); + max-width: 220px; display: inline-block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - cursor: pointer; + cursor: copy; + transition: all .12s var(--ease); +} +.match-value:hover { + background: rgba(69,208,196,.12); + border-color: var(--teal); + box-shadow: 0 0 12px rgba(69,208,196,.12); } -.td-provider { - font-weight: 600; - color: #aaa; -} +.td-provider { font-weight: 700; color: var(--text); font-size: 10.5px; letter-spacing: .04em; max-width: 130px; } +.td-pattern { color: var(--text-dim); font-size: 10px; max-width: 170px; } +.td-domain { color: var(--amber); font-size: 10.5px; max-width: 180px; } +.td-source { max-width: 200px; } +.td-time { color: var(--text-faint); font-size: 10px; white-space: nowrap; } -.td-pattern { - color: #888; - font-size: 11px; -} +a { color: var(--teal); text-decoration: none; border-bottom: 1px dashed rgba(69,208,196,.35); } +a:hover { color: #8be8de; border-bottom-style: solid; } -.td-domain { - color: #4a9eff; -} - -.td-time { - color: #555; - font-size: 11px; - white-space: nowrap; -} - -a { - color: #4a9eff; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} +/* row actions — revealed on row hover */ +.td-actions { white-space: nowrap; text-align: right; } .btn-icon { + font-family: var(--mono); padding: 3px 8px; - background: #1a1a1a; - border: 1px solid #2a2a2a; - border-radius: 4px; - color: #888; - font-size: 10px; + background: transparent; + border: 1px solid var(--line-strong); + border-radius: 2px; + color: var(--text-faint); + font-size: 9px; + font-weight: 700; + letter-spacing: .08em; + text-transform: uppercase; cursor: pointer; margin-right: 4px; - transition: all 0.1s; + opacity: .35; + transition: all .12s var(--ease); } +.findings-table tbody tr:hover .btn-icon, +.btn-icon:focus-visible { opacity: 1; } +.btn-icon:hover { color: var(--amber-hot); border-color: var(--amber); background: rgba(245,165,36,.06); } +.btn-icon.copied { color: var(--green); border-color: var(--green); } +.btn-icon-danger:hover { color: var(--red); border-color: var(--red); background: rgba(255,92,92,.08); } -.btn-icon:hover { - background: #222; - color: #ccc; +/* skeleton loading rows */ +.skl-row td { padding: 9px 12px; } +.skl { + height: 9px; + border-radius: 2px; + background: linear-gradient(90deg, #0c1119 25%, #151c2b 50%, #0c1119 75%); + background-size: 200% 100%; + animation: shimmer 1.15s linear infinite; } +.skl-w1 { width: 34%; } .skl-w2 { width: 62%; } .skl-w3 { width: 48%; } .skl-w4 { width: 78%; } +@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } } -.btn-icon-danger:hover { - background: #2a1515; - color: #ff6b6b; - border-color: #4a2020; -} +/* ------------------------------------------------ empty state */ .empty-state { text-align: center; - padding: 60px 20px; - color: #444; + padding: 56px 20px 50px; + color: var(--text-faint); } -.empty-icon { - font-size: 48px; - margin-bottom: 16px; - opacity: 0.3; +.empty-radar { width: 72px; height: 72px; margin-bottom: 18px; opacity: .8; } +.er-ring { fill: none; stroke: var(--line-strong); stroke-width: 1; } +.er-cross { stroke: var(--line); stroke-width: 1; } +.er-core { fill: var(--teal); } +.er-sweep { + fill: rgba(245,165,36,.14); + transform-origin: 36px 36px; + animation: radar-sweep 4.2s linear infinite; } -.empty-state p { - font-size: 14px; - max-width: 400px; - margin: 0 auto; - line-height: 1.6; +.empty-title { + font-size: 12px; + font-weight: 700; + letter-spacing: .18em; + text-transform: uppercase; + color: var(--text-dim); + margin-bottom: 8px; } +.empty-copy { + font-size: 10.5px; + max-width: 420px; + margin: 0 auto 12px; + line-height: 1.7; + color: var(--text-faint); +} +.empty-hint { + font-size: 9.5px; + letter-spacing: .1em; + color: var(--amber); + opacity: .75; +} +.empty-hint::before { content: "» "; } -::-webkit-scrollbar { - width: 6px; - height: 6px; -} +/* ------------------------------------------------ page footer */ -::-webkit-scrollbar-track { - background: transparent; +.page-footer { + margin-top: 14px; + text-align: center; + font-size: 9px; + letter-spacing: .14em; + color: var(--text-faint); + text-transform: uppercase; + opacity: .7; } +.footer-sep { margin: 0 8px; color: var(--line-strong); } -::-webkit-scrollbar-thumb { - background: #333; - border-radius: 4px; +/* ------------------------------------------------ scrollbars */ + +::-webkit-scrollbar { width: 7px; height: 7px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: #1c2536; border-radius: 4px; } +::-webkit-scrollbar-thumb:hover { background: #2a3650; } + +/* ------------------------------------------------ reduced motion */ + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: .01ms !important; + animation-iteration-count: 1 !important; + transition-duration: .01ms !important; + } } diff --git a/js/popup.js b/js/popup.js index c44c64a..d5db848 100644 --- a/js/popup.js +++ b/js/popup.js @@ -1,23 +1,71 @@ 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 = ""; - document.getElementById("keywordCount").textContent = keywords.length; + lastKeywords = keywords.length; + setNumber(document.getElementById("keywordCount"), keywords.length); + if (termBooted) updateTermLine(false); if (keywords.length === 0) { - list.innerHTML = '
  • No keywords configured
  • '; + const empty = document.createElement("li"); + empty.className = "empty-state"; + empty.textContent = "no keywords configured"; + list.appendChild(empty); return; } @@ -31,7 +79,7 @@ async function renderKeywords() { const removeBtn = document.createElement("button"); removeBtn.className = "keyword-remove"; - removeBtn.textContent = "\u00D7"; + removeBtn.textContent = "×"; removeBtn.title = `Remove "${kw}"`; removeBtn.addEventListener("click", () => handleRemoveKeyword(kw)); @@ -44,7 +92,9 @@ async function renderKeywords() { async function renderStats() { const response = await chrome.runtime.sendMessage({ type: "getFindings" }); const findings = response.findings || []; - document.getElementById("findingCount").textContent = findings.length; + lastFindings = findings.length; + setNumber(document.getElementById("findingCount"), findings.length); + if (termBooted) updateTermLine(false); } async function handleAddKeyword(e) { @@ -68,6 +118,7 @@ async function handleAddKeyword(e) { } input.value = ""; + input.focus(); await renderKeywords(); } diff --git a/js/results.js b/js/results.js index a5af5a0..caad97c 100644 --- a/js/results.js +++ b/js/results.js @@ -1,16 +1,24 @@ let allFindings = []; +let firstRenderDone = false; + +const REDUCE_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)").matches; document.addEventListener("DOMContentLoaded", init); async function init() { const versionLabel = document.getElementById("versionLabel"); if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version; + + renderSkeleton(); + const response = await chrome.runtime.sendMessage({ type: "getFindings" }); allFindings = response.findings || []; populateFilters(); renderStats(); renderFindings(); + renderTicker(); + firstRenderDone = true; document.getElementById("severityFilter").addEventListener("change", renderFindings); document.getElementById("typeFilter").addEventListener("change", renderFindings); @@ -19,6 +27,57 @@ async function init() { document.getElementById("exportJsonBtn").addEventListener("click", exportJson); document.getElementById("exportCsvBtn").addEventListener("click", exportCsv); document.getElementById("clearBtn").addEventListener("click", clearAll); + + const searchBox = document.getElementById("searchBox"); + document.addEventListener("keydown", (e) => { + const tag = document.activeElement && document.activeElement.tagName; + const typing = tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA"; + if (e.key === "/" && !typing) { + e.preventDefault(); + searchBox.focus(); + } else if (e.key === "Escape" && document.activeElement === searchBox) { + searchBox.value = ""; + renderFindings(); + searchBox.blur(); + } + }); +} + +/* 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 = 400; + 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); +} + +function renderSkeleton() { + const tbody = document.getElementById("findingsBody"); + tbody.classList.remove("fresh"); + tbody.innerHTML = ""; + const widths = ["skl-w2", "skl-w1", "skl-w3", "skl-w4", "skl-w2", "skl-w3"]; + for (let i = 0; i < 6; i++) { + const tr = document.createElement("tr"); + tr.className = "skl-row"; + const td = document.createElement("td"); + td.colSpan = 10; + const bar = document.createElement("div"); + bar.className = "skl " + widths[i % widths.length]; + td.appendChild(bar); + tr.appendChild(td); + tbody.appendChild(tr); + } } function getFiltered() { @@ -67,54 +126,96 @@ function renderStats() { bar.innerHTML = ""; const stats = [ - { label: "Total", value: allFindings.length, cls: "stat-total" }, - { label: "Critical", value: critical, cls: "stat-critical" }, - { label: "High", value: high, cls: "stat-high" }, - { label: "Medium", value: medium, cls: "stat-medium" }, - { label: "Low", value: low, cls: "stat-low" }, - { label: "Domains", value: domains, cls: "stat-domains" }, + { label: "Total", value: allFindings.length, cls: "stat-total", filter: "all" }, + { label: "Critical", value: critical, cls: "stat-critical", filter: "critical" }, + { label: "High", value: high, cls: "stat-high", filter: "high" }, + { label: "Medium", value: medium, cls: "stat-medium", filter: "medium" }, + { label: "Low", value: low, cls: "stat-low", filter: "low" }, + { label: "Domains", value: domains, cls: "stat-domains", filter: null }, ]; for (const s of stats) { const el = document.createElement("div"); el.className = `stat-item ${s.cls}`; - const num = document.createElement("span"); - num.className = "stat-num"; - num.textContent = s.value; + + const top = document.createElement("span"); + top.className = "stat-top"; + const led = document.createElement("span"); + led.className = "stat-led"; const lbl = document.createElement("span"); lbl.className = "stat-lbl"; lbl.textContent = s.label; + top.appendChild(led); + top.appendChild(lbl); + + const num = document.createElement("span"); + num.className = "stat-num"; + + el.appendChild(top); el.appendChild(num); - el.appendChild(lbl); + setNumber(num, s.value); + + if (s.filter) { + el.dataset.filter = s.filter; + el.title = s.filter === "all" ? "Show all severities" : `Filter: ${s.label.toLowerCase()} only`; + el.addEventListener("click", () => { + document.getElementById("severityFilter").value = s.filter; + renderFindings(); + }); + } bar.appendChild(el); } } +function syncStatActive() { + const current = document.getElementById("severityFilter").value; + document.querySelectorAll(".stat-item[data-filter]").forEach((el) => { + el.classList.toggle("active", el.dataset.filter === current); + }); +} + function renderFindings() { const filtered = getFiltered(); const tbody = document.getElementById("findingsBody"); const empty = document.getElementById("emptyState"); tbody.innerHTML = ""; + tbody.classList.toggle("fresh", !firstRenderDone && !REDUCE_MOTION); + + updateMeta(filtered.length); + syncStatActive(); if (filtered.length === 0) { + const title = document.getElementById("emptyTitle"); + const copy = document.getElementById("emptyCopy"); + if (allFindings.length === 0) { + title.textContent = "No findings yet"; + copy.textContent = "KeyFinder passively scans every page you visit. Browse around — leaked keys, tokens and credentials will surface here."; + } else { + title.textContent = "No matches in view"; + copy.textContent = "The current filters hide every finding. Adjust or clear them to bring results back."; + } empty.hidden = false; return; } empty.hidden = true; const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 }; - filtered.sort((a, b) => (severityOrder[a.severity] || 5) - (severityOrder[b.severity] || 5)); + filtered.sort((a, b) => (severityOrder[a.severity] ?? 5) - (severityOrder[b.severity] ?? 5)); filtered.forEach((f, i) => { + const sev = f.severity || "medium"; const tr = document.createElement("tr"); + tr.className = "sev-" + sev; + if (i < 30) tr.style.setProperty("--i", i); const tdNum = document.createElement("td"); - tdNum.textContent = i + 1; + tdNum.textContent = String(i + 1).padStart(2, "0"); + tdNum.className = "td-num"; const tdSev = document.createElement("td"); const badge = document.createElement("span"); - badge.className = `badge badge-${f.severity || "medium"}`; - badge.textContent = (f.severity || "medium").toUpperCase(); + badge.className = `badge badge-${sev}`; + badge.textContent = sev.toUpperCase(); tdSev.appendChild(badge); const tdProvider = document.createElement("td"); @@ -129,7 +230,11 @@ function renderFindings() { const matchCode = document.createElement("code"); matchCode.textContent = f.match || "-"; matchCode.className = "match-value"; - matchCode.title = f.match || ""; + matchCode.title = "click to copy\n" + (f.match || ""); + matchCode.addEventListener("click", () => { + navigator.clipboard.writeText(f.match || ""); + flashCopied(copyBtn); + }); tdMatch.appendChild(matchCode); const tdType = document.createElement("td"); @@ -143,42 +248,45 @@ function renderFindings() { tdDomain.className = "td-domain"; const tdSource = document.createElement("td"); + tdSource.className = "td-source"; if (f.url && f.url.startsWith("http")) { const a = document.createElement("a"); a.href = f.url; a.target = "_blank"; a.rel = "noopener"; - a.textContent = truncateUrl(f.url, 40); + a.textContent = truncateUrl(f.url, 30); a.title = f.url; tdSource.appendChild(a); } else { - tdSource.textContent = f.url ? truncateUrl(f.url, 40) : "-"; + tdSource.textContent = f.url ? truncateUrl(f.url, 30) : "-"; } const tdTime = document.createElement("td"); tdTime.textContent = f.timestamp ? formatTime(f.timestamp) : "-"; tdTime.className = "td-time"; + if (f.timestamp) tdTime.title = new Date(f.timestamp).toLocaleString(); const tdActions = document.createElement("td"); + tdActions.className = "td-actions"; const copyBtn = document.createElement("button"); copyBtn.className = "btn-icon"; - copyBtn.textContent = "Copy"; + copyBtn.textContent = "copy"; copyBtn.title = "Copy match value"; copyBtn.addEventListener("click", () => { navigator.clipboard.writeText(f.match || ""); - copyBtn.textContent = "Done"; - setTimeout(() => (copyBtn.textContent = "Copy"), 1500); + flashCopied(copyBtn); }); const delBtn = document.createElement("button"); delBtn.className = "btn-icon btn-icon-danger"; - delBtn.textContent = "Del"; + delBtn.textContent = "del"; delBtn.title = "Remove finding"; delBtn.addEventListener("click", async () => { await chrome.runtime.sendMessage({ type: "removeFinding", findingId: f.id }); allFindings = allFindings.filter((x) => x.id !== f.id); renderStats(); renderFindings(); + renderTicker(); }); tdActions.appendChild(copyBtn); @@ -198,6 +306,58 @@ function renderFindings() { }); } +function flashCopied(btn) { + if (!btn) return; + btn.textContent = "copied"; + btn.classList.add("copied"); + setTimeout(() => { + btn.textContent = "copy"; + btn.classList.remove("copied"); + }, 1200); +} + +function updateMeta(shown) { + const metaLine = document.getElementById("metaLine"); + if (metaLine) { + metaLine.innerHTML = ""; + metaLine.append( + "showing ", + strong(shown), + " of ", + strong(allFindings.length) + ); + } + const shellMeta = document.getElementById("shellMeta"); + if (shellMeta) shellMeta.textContent = shown + (shown === 1 ? " row" : " rows"); +} + +function strong(n) { + const el = document.createElement("strong"); + el.textContent = n; + return el; +} + +function renderTicker() { + const ticker = document.getElementById("ticker"); + const tickerText = document.getElementById("tickerText"); + if (!ticker || !tickerText) return; + if (!allFindings.length) { + ticker.hidden = true; + return; + } + const latest = [...allFindings].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0]; + tickerText.innerHTML = ""; + const head = document.createElement("span"); + head.textContent = `${latest.provider || "unknown"} · ${latest.patternName || "match"} @ ${latest.domain || "—"} `; + tickerText.appendChild(head); + if (latest.match) { + const code = document.createElement("code"); + code.textContent = truncateUrl(latest.match, 28); + tickerText.appendChild(code); + } + ticker.hidden = false; +} + function truncateUrl(url, max) { if (url.length <= max) return url; return url.substring(0, max - 3) + "..."; @@ -205,7 +365,15 @@ function truncateUrl(url, max) { function formatTime(ts) { const d = new Date(ts); - return d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + const diff = Date.now() - d.getTime(); + const m = Math.floor(diff / 60000); + if (m < 1) return "just now"; + if (m < 60) return m + "m ago"; + const h = Math.floor(m / 60); + if (h < 24) return h + "h ago"; + const days = Math.floor(h / 24); + if (days < 7) return days + "d ago"; + return d.toLocaleDateString(); } function exportJson() { @@ -255,4 +423,5 @@ async function clearAll() { allFindings = []; renderStats(); renderFindings(); + renderTicker(); } diff --git a/manifest.firefox.json b/manifest.firefox.json index 6c9a309..23ad668 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -1,7 +1,7 @@ { "name": "KeyFinder", "description": "Passively discovers API keys, tokens, and secrets leaked in page scripts, DOM, network responses, and browser storage. Available for Chrome and Firefox.", - "version": "2.1.1", + "version": "2.2.0", "manifest_version": 3, "browser_specific_settings": { "gecko": { diff --git a/manifest.json b/manifest.json index cae3a56..cb9b09a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "KeyFinder", "description": "Passively discovers API keys, tokens, and secrets leaked in page scripts, DOM, network responses, and browser storage. Available for Chrome and Firefox.", - "version": "2.1.1", + "version": "2.2.0", "manifest_version": 3, "action": { "default_icon": { diff --git a/popup.html b/popup.html index 8ddb08f..2187b27 100644 --- a/popup.html +++ b/popup.html @@ -9,31 +9,36 @@
    - KeyFinder -

    KeyFinder

    - + +
    +

    KEYFINDER

    +

    passive secret discovery

    +
    + ARMED
    -

    Passive API key & secret discovery

    -
    - - - Findings +
    + Findings +
    -
    - - - Keywords +
    + Keywords +
    -

    Keywords

    +

    Watchlist// custom keywords

    + @@ -44,8 +49,9 @@
    diff --git a/preview/popup-preview.html b/preview/popup-preview.html new file mode 100644 index 0000000..bf019db --- /dev/null +++ b/preview/popup-preview.html @@ -0,0 +1,61 @@ + + + + + + KeyFinder + + + +
    +
    + +
    +

    KEYFINDER

    +

    passive secret discovery

    +
    + ARMED +
    +
    + +
    +
    + Findings + +
    +
    + Keywords + +
    +
    + +
    +

    Watchlist// custom keywords

    + + + + + + + +
    + + + + + + + diff --git a/preview/preview-stub.js b/preview/preview-stub.js new file mode 100644 index 0000000..b8e17d2 --- /dev/null +++ b/preview/preview-stub.js @@ -0,0 +1,46 @@ +// Preview-only stub for the chrome.* extension API with realistic sample data. +// Loaded before the real popup.js / results.js in preview/*.html files. +// NOT shipped — build.sh only copies the explicit SHARED_FILES list. +(function () { + const NOW = Date.now(); + const MIN = 60 * 1000; + const HOUR = 60 * MIN; + const DAY = 24 * HOUR; + + const SAMPLE_FINDINGS = [ + { id: "f1", severity: "critical", provider: "AWS", patternName: "AWS Access Key ID", match: "AKIAIOSFODNN7EXAMPLE", type: "inline-script", domain: "dashboard.acme.io", url: "https://dashboard.acme.io/assets/app.js", pageUrl: "https://dashboard.acme.io/", timestamp: NOW - 4 * MIN }, + { id: "f2", severity: "critical", provider: "Stripe", patternName: "Stripe Live Secret Key", match: "sk_live_4eC39HqLyj…T1zdp7dc", type: "network-response", domain: "api.shopfront.dev", url: "https://api.shopfront.dev/v2/config", pageUrl: "https://shopfront.dev/", timestamp: NOW - 26 * MIN }, + { id: "f3", severity: "critical", provider: "OpenAI", patternName: "OpenAI API Key", match: "sk-proj-abc123def456ghi789jkl", type: "web-storage", domain: "playground.internal.tools", url: "https://playground.internal.tools/", pageUrl: "https://playground.internal.tools/", timestamp: NOW - 2 * HOUR }, + { id: "f4", severity: "high", provider: "GitHub", patternName: "GitHub Personal Access Token", match: "ghp_16C7e42F292c6912E7710c838347Ae178B4a", type: "html-comment", domain: "gitmirror.example.org", url: "https://gitmirror.example.org/login", pageUrl: "https://gitmirror.example.org/", timestamp: NOW - 3 * HOUR }, + { id: "f5", severity: "high", provider: "Slack", patternName: "Slack Bot Token", match: "xoxb-17653672481-19874698323-pdLjLl…CwXgE", type: "inline-script", domain: "status.teamchat.app", url: "https://status.teamchat.app/embed.js", pageUrl: "https://status.teamchat.app/", timestamp: NOW - 5 * HOUR }, + { id: "f6", severity: "high", provider: "MongoDB", patternName: "MongoDB Connection String", match: "mongodb+srv://admin:p4ssw0rd@cluster0.mongodb.net/prod", type: "data-attribute", domain: "metrics.saasly.co", url: "https://metrics.saasly.co/", pageUrl: "https://metrics.saasly.co/", timestamp: NOW - 9 * HOUR }, + { id: "f7", severity: "high", provider: "DOM Scan", patternName: "Sensitive Meta Tag", match: "csrf-token=9f8e7d6c5b4a3210", type: "meta-tag", domain: "legacy.corpnet.com", url: "https://legacy.corpnet.com/index.html", pageUrl: "https://legacy.corpnet.com/", timestamp: NOW - 14 * HOUR }, + { id: "f8", severity: "medium", provider: "Generic", patternName: "JSON Web Token (JWT)", match: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U", type: "url-param", domain: "auth.redirectlab.net", url: "https://auth.redirectlab.net/callback?token=eyJhbGciOi...", pageUrl: "https://redirectlab.net/", timestamp: NOW - 20 * HOUR }, + { id: "f9", severity: "medium", provider: "DOM Scan", patternName: "Hidden Form Field", match: "session_id=a8f5f167f44f4964e6c998dee827110c", type: "hidden-input", domain: "shop.example.com", url: "https://shop.example.com/checkout", pageUrl: "https://shop.example.com/", timestamp: NOW - 1 * DAY }, + { id: "f10", severity: "medium", provider: "URL Scan", patternName: "High-Entropy URL Parameter", match: "sig=4c4f6e6f2d9a4b8c8d7e6f5a4b3c2d1e", type: "url-param", domain: "cdn.medialab.io", url: "https://cdn.medialab.io/v/assets?sig=4c4f6e...", pageUrl: "https://medialab.io/", timestamp: NOW - 2 * DAY }, + { id: "f11", severity: "medium", provider: "Web Storage", patternName: "localStorage Secret", match: "refresh_token=def502001a2b3c4d5e6f", type: "web-storage", domain: "app.notesync.dev", url: "https://app.notesync.dev/", pageUrl: "https://app.notesync.dev/", timestamp: NOW - 3 * DAY }, + { id: "f12", severity: "low", provider: "Generic", patternName: "Keyword: client_id", match: "client_id=Iv1.8a61f9b3a7aba766", type: "script-src", domain: "login.oauthflow.app", url: "https://login.oauthflow.app/js/oauth.js?client_id=Iv1.8a61f9b3a7aba766", pageUrl: "https://login.oauthflow.app/", timestamp: NOW - 4 * DAY }, + ]; + + const SAMPLE_KEYWORDS = [ + "key", "api_key", "apikey", "api-key", "secret", "token", + "access_token", "auth", "credential", "password", "client_id", "client_secret", + ]; + + window.chrome = { + runtime: { + getManifest: () => ({ version: "2.1.1" }), + sendMessage: async (msg) => { + switch (msg && msg.type) { + case "getFindings": return { findings: SAMPLE_FINDINGS.slice() }; + case "getKeywords": return { keywords: SAMPLE_KEYWORDS.slice() }; + case "addKeyword": return { ok: true }; + case "removeKeyword": return { ok: true }; + case "removeFinding": return { ok: true }; + case "clearFindings": return { ok: true }; + default: return {}; + } + }, + }, + }; +})(); diff --git a/preview/results-preview.html b/preview/results-preview.html new file mode 100644 index 0000000..3163598 --- /dev/null +++ b/preview/results-preview.html @@ -0,0 +1,122 @@ + + + + + + KeyFinder — Findings Console + + + +
    +
    +
    + +
    +

    KEYFINDER

    +

    findings console

    +
    + PASSIVE +
    +
    +
    + + + / +
    +
    + + + +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    + +
    + + + +
    +
    +
    + findings.log + +
    + + + + + + + + + + + + + + + + +
    #SeverityProviderPatternMatchTypeDomainSourceTime
    + +
    + +
    + + + + + diff --git a/preview/shot-empty.png b/preview/shot-empty.png new file mode 100644 index 0000000..a48f594 Binary files /dev/null and b/preview/shot-empty.png differ diff --git a/preview/shot-popup.png b/preview/shot-popup.png new file mode 100644 index 0000000..eecc203 Binary files /dev/null and b/preview/shot-popup.png differ diff --git a/preview/shot-results.png b/preview/shot-results.png new file mode 100644 index 0000000..7390eac Binary files /dev/null and b/preview/shot-results.png differ diff --git a/results.html b/results.html index 5368f7a..1e42710 100644 --- a/results.html +++ b/results.html @@ -3,64 +3,117 @@ - KeyFinder - Findings + KeyFinder — Findings Console
    -
    - KeyFinder -

    KeyFinder

    +
    +
    + +
    +

    KEYFINDER

    +

    findings console

    +
    + PASSIVE +
    +
    +
    + + + / +
    +
    + + + +
    +
    -
    +
    - - - - -
    -
    - - - + + +
    +
    + +
    - - - - - - - - - - - - - - - - -
    #SeverityProviderPatternMatchTypeDomainSourceTime
    -