Add minimal SVG icon system for signal types

Replace emoji icons with inline SVG for WiFi, Bluetooth, and RF/SDR
indicators. Icons are standard symbols (arc, rune, wave) designed for
screenshot legibility in reports.

- Add Icons utility object in utils.js with SVG generators
- Add icon CSS system with sizing variants and state animations
- Update TSCM scanner indicators and capabilities bar
- Remove decorative sensor type emojis (text labels suffice)
- Keep signal strength SVG bars (already implemented)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-20 22:05:58 +00:00
parent c92f60e0f3
commit 256c30e7cd
5 changed files with 334 additions and 35 deletions

View File

@@ -631,6 +631,18 @@
color: var(--text-muted);
font-size: 12px;
}
.tscm-empty-primary {
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 6px;
}
.tscm-empty-secondary {
font-size: 10px;
color: var(--text-muted);
max-width: 280px;
margin: 0 auto;
line-height: 1.4;
}
/* Futuristic Scanner Progress */
.tscm-scanner-progress {
@@ -795,6 +807,17 @@
}
.cap-icon {
font-size: 14px;
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.cap-icon svg {
width: 100%;
height: 100%;
stroke: currentColor;
fill: none;
}
.cap-status {
color: var(--text-muted);
@@ -1164,6 +1187,14 @@
font-size: 24px;
display: block;
margin-bottom: 8px;
width: 24px;
height: 24px;
}
.cap-detail-item .cap-icon svg {
width: 100%;
height: 100%;
stroke: currentColor;
fill: none;
}
.cap-detail-item .cap-name {
font-weight: 600;
@@ -1256,3 +1287,156 @@
border-radius: 3px;
margin-left: 8px;
}
/* ==========================================================================
Icon System
Minimal, functional icons that replace words. No decoration.
Designed for screenshot legibility in reports.
========================================================================== */
.icon {
display: inline-block;
width: 16px;
height: 16px;
vertical-align: middle;
flex-shrink: 0;
}
.icon svg {
width: 100%;
height: 100%;
fill: currentColor;
}
.icon--sm {
width: 12px;
height: 12px;
}
.icon--lg {
width: 20px;
height: 20px;
}
/* Signal Type Icons */
.icon-wifi svg,
.icon-bluetooth svg,
.icon-cellular svg,
.icon-signal-unknown svg {
fill: var(--text-secondary);
}
/* Recording State */
.icon-recording {
color: #ff3366;
}
.icon-recording.active svg {
animation: recording-pulse 1.5s ease-in-out infinite;
}
@keyframes recording-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* Anomaly Indicator */
.icon-anomaly {
color: #ff9933;
}
.icon-anomaly.critical {
color: #ff3366;
}
/* Export Icon */
.icon-export {
color: var(--text-secondary);
}
/* Device Indicators with Icons */
.device-indicator-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
opacity: 0.3;
transition: opacity 0.3s, transform 0.3s;
}
.device-indicator-icon.active {
opacity: 1;
animation: device-pulse 1.5s ease-in-out infinite;
}
.device-indicator-icon.inactive {
opacity: 0.2;
}
.device-indicator-icon .icon {
width: 18px;
height: 18px;
}
/* Protocol badge with icon */
.protocol-icon-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
padding: 2px 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
text-transform: uppercase;
}
.protocol-icon-badge .icon {
width: 12px;
height: 12px;
}
/* Recording status indicator */
.recording-status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
}
.recording-status .icon-recording {
width: 10px;
height: 10px;
}
.recording-status.active {
color: #ff3366;
font-weight: 600;
}
/* Anomaly flag in device items */
.anomaly-flag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 6px;
border-radius: 3px;
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
}
.anomaly-flag.needs-review {
background: rgba(255, 153, 51, 0.2);
color: #ff9933;
}
.anomaly-flag.high-interest {
background: rgba(255, 51, 51, 0.2);
color: #ff3333;
}
.anomaly-flag .icon {
width: 10px;
height: 10px;
}

View File

@@ -833,20 +833,10 @@ const SignalCards = (function() {
? createSignalIndicator(rssi, { compact: true })
: '<span class="signal-strength-indicator compact no-data" title="No signal data available">--</span>';
// Determine sensor type icon
let sensorIcon = '📡';
const model = (msg.model || '').toLowerCase();
if (model.includes('weather') || msg.temperature !== undefined) sensorIcon = '🌡️';
else if (model.includes('door') || model.includes('window')) sensorIcon = '🚪';
else if (model.includes('motion') || model.includes('pir')) sensorIcon = '🚶';
else if (model.includes('smoke') || model.includes('fire')) sensorIcon = '🔥';
else if (model.includes('water') || model.includes('leak')) sensorIcon = '💧';
else if (model.includes('car') || model.includes('tire') || model.includes('tpms')) sensorIcon = '🚗';
card.innerHTML = `
<div class="signal-card-header">
<div class="signal-card-badges">
<span class="signal-proto-badge sensor">${sensorIcon} ${escapeHtml(msg.model || 'Unknown')}</span>
<span class="signal-proto-badge sensor">${escapeHtml(msg.model || 'Unknown')}</span>
<span class="signal-freq-badge">ID: ${escapeHtml(msg.id || 'N/A')}</span>
${signalIndicator}
</div>

View File

@@ -271,3 +271,125 @@ function clamp(num, min, max) {
function mapRange(value, inMin, inMax, outMin, outMax) {
return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
}
// ============== ICON SYSTEM ==============
// Minimal SVG icons. Each returns HTML string.
// Designed for screenshot legibility - standard symbols only.
const Icons = {
/**
* WiFi icon - standard arc/fan shape
*/
wifi: function(className) {
return `<span class="icon icon-wifi ${className || ''}" aria-label="WiFi">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12.55a11 11 0 0 1 14.08 0"/>
<path d="M1.42 9a16 16 0 0 1 21.16 0"/>
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"/>
<circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/>
</svg>
</span>`;
},
/**
* Bluetooth icon - standard rune
*/
bluetooth: function(className) {
return `<span class="icon icon-bluetooth ${className || ''}" aria-label="Bluetooth">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/>
</svg>
</span>`;
},
/**
* Cellular icon - ascending bars
*/
cellular: function(className) {
return `<span class="icon icon-cellular ${className || ''}" aria-label="Cellular">
<svg viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="16" width="4" height="6" rx="1"/>
<rect x="8" y="12" width="4" height="10" rx="1"/>
<rect x="14" y="8" width="4" height="14" rx="1"/>
<rect x="20" y="4" width="4" height="18" rx="1" opacity="0.3"/>
</svg>
</span>`;
},
/**
* Unknown/RF signal - generic wave
*/
signalUnknown: function(className) {
return `<span class="icon icon-signal-unknown ${className || ''}" aria-label="Unknown signal">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M2 12c0-3 2-6 5-6s4 3 5 6c1 3 2 6 5 6s5-3 5-6"/>
</svg>
</span>`;
},
/**
* Recording indicator - filled circle
*/
recording: function(className) {
return `<span class="icon icon-recording ${className || ''}" aria-label="Recording">
<svg viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="8"/>
</svg>
</span>`;
},
/**
* Anomaly indicator - filled circle (amber by default via CSS)
*/
anomaly: function(className) {
return `<span class="icon icon-anomaly ${className || ''}" aria-label="Anomaly">
<svg viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="6"/>
</svg>
</span>`;
},
/**
* Export icon - arrow out of box
*/
export: function(className) {
return `<span class="icon icon-export ${className || ''}" aria-label="Export">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
</span>`;
},
/**
* Refresh icon - circular arrows
*/
refresh: function(className) {
return `<span class="icon icon-refresh ${className || ''}" aria-label="Refresh">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"/>
<polyline points="1 20 1 14 7 14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
</span>`;
},
/**
* Get icon by signal type
* Maps protocol names to appropriate icons
*/
forSignalType: function(type, className) {
const t = (type || '').toLowerCase();
if (t.includes('wifi') || t.includes('802.11')) {
return this.wifi(className);
}
if (t.includes('bluetooth') || t.includes('bt') || t.includes('ble')) {
return this.bluetooth(className);
}
if (t.includes('cellular') || t.includes('lte') || t.includes('gsm') || t.includes('5g')) {
return this.cellular(className);
}
return this.signalUnknown(className);
}
};