mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Add Bluetooth UI polish design spec
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -67,3 +67,4 @@ data/subghz/captures/
|
||||
|
||||
# Local utility scripts
|
||||
reset-sdr.*
|
||||
.superpowers/
|
||||
|
||||
225
docs/superpowers/specs/2026-03-27-bluetooth-ui-polish-design.md
Normal file
225
docs/superpowers/specs/2026-03-27-bluetooth-ui-polish-design.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Bluetooth UI Polish — Apply WiFi Treatment
|
||||
|
||||
**Date:** 2026-03-27
|
||||
**Status:** Approved
|
||||
**Scope:** Frontend only — CSS, JS, HTML. No backend changes.
|
||||
|
||||
## Overview
|
||||
|
||||
Apply the same visual polish introduced in the WiFi scanner redesign to the Bluetooth scanner. Three areas are updated: device rows, proximity radar, and the device list header. The signal distribution strip is retained.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|---|---|---|
|
||||
| Row structure | Match WiFi 2-line layout closely | Visual consistency across modes |
|
||||
| Locate button placement | Remove from rows, keep in detail panel only | Rows are already information-dense; locate is a detail-panel action |
|
||||
| Radar sweep | CSS animation + trailing glow arc | Matches WiFi, replaces rAF loop in ProximityRadar |
|
||||
| Header additions | Scan indicator + sort controls | Matches WiFi; sort by Signal / Name / Seen / Distance |
|
||||
| Signal distribution strip | Keep | Provides a useful at-a-glance signal breakdown not present in WiFi |
|
||||
| Sort/filter layout | Sort group + filter group on one combined controls row | Saves vertical space vs separate rows |
|
||||
|
||||
## CSS Tokens
|
||||
|
||||
Reuse tokens already defined in `index.css :root` — no new tokens needed:
|
||||
`--bg-primary`, `--bg-secondary`, `--bg-tertiary`, `--bg-card`, `--border-color`, `--border-light`, `--text-primary`, `--text-secondary`, `--text-dim`, `--accent-cyan`, `--accent-cyan-dim`, `--accent-green`, `--accent-green-dim`, `--accent-red`, `--accent-red-dim`, `--accent-orange`, `--accent-amber-dim`.
|
||||
|
||||
Hardcoded literals used in WiFi are reused here for consistency:
|
||||
- Selected row tint: `rgba(74, 163, 255, 0.07)`
|
||||
- Radar arc fill: `rgba(0, 180, 216, N)` (same as WiFi's `#00b4d8`)
|
||||
|
||||
## Component Designs
|
||||
|
||||
### 1. Device List Header
|
||||
|
||||
**Scan indicator** — new `<div class="bt-scan-indicator" id="btScanIndicator">` added to `.wifi-device-list-header` (right-aligned via `margin-left: auto`):
|
||||
```html
|
||||
<div class="bt-scan-indicator" id="btScanIndicator">
|
||||
<span class="bt-scan-dot"></span>
|
||||
<span class="bt-scan-text">IDLE</span>
|
||||
</div>
|
||||
```
|
||||
`.bt-scan-dot` is 7×7px circle, `animation: bt-scan-pulse 1.2s ease-in-out infinite` (opacity/scale identical to WiFi). Dot hidden when idle (same as WiFi pattern).
|
||||
|
||||
`setScanning(scanning)` updated — add after the existing start/stop btn toggle:
|
||||
```js
|
||||
const dot = document.getElementById('btScanIndicator')?.querySelector('.bt-scan-dot');
|
||||
const text = document.getElementById('btScanIndicator')?.querySelector('.bt-scan-text');
|
||||
if (dot) dot.style.display = scanning ? 'inline-block' : 'none';
|
||||
if (text) text.textContent = scanning ? 'SCANNING' : 'IDLE';
|
||||
```
|
||||
|
||||
### 2. Sort + Filter Controls Row
|
||||
|
||||
**Replaces** the separate `.bt-device-toolbar` (search stays above) and `.bt-device-filters` divs. A new combined controls row sits between the signal strip and the search input:
|
||||
|
||||
```html
|
||||
<div class="bt-controls-row">
|
||||
<div class="bt-sort-group">
|
||||
<span class="bt-sort-label">Sort</span>
|
||||
<button class="bt-sort-btn active" data-sort="rssi">Signal</button>
|
||||
<button class="bt-sort-btn" data-sort="name">Name</button>
|
||||
<button class="bt-sort-btn" data-sort="seen">Seen</button>
|
||||
<button class="bt-sort-btn" data-sort="distance">Dist</button>
|
||||
</div>
|
||||
<div class="bt-filter-group">
|
||||
<button class="bt-filter-btn active" data-filter="all">All</button>
|
||||
<button class="bt-filter-btn" data-filter="new">New</button>
|
||||
<button class="bt-filter-btn" data-filter="named">Named</button>
|
||||
<button class="bt-filter-btn" data-filter="strong">Strong</button>
|
||||
<button class="bt-filter-btn" data-filter="trackers">Trackers</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Sort state:** add `let sortBy = 'rssi'` to module-level state. Sort button click handler (bound in `init()`):
|
||||
```js
|
||||
document.querySelectorAll('.bt-sort-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
sortBy = btn.dataset.sort;
|
||||
document.querySelectorAll('.bt-sort-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
renderAllDevices();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**`renderAllDevices()`** — new function that re-renders all devices in sorted order:
|
||||
```js
|
||||
function renderAllDevices() {
|
||||
if (!deviceContainer) return;
|
||||
deviceContainer.innerHTML = '';
|
||||
const sorted = [...devices.values()].sort((a, b) => {
|
||||
if (sortBy === 'rssi') return (b.rssi_current ?? -100) - (a.rssi_current ?? -100);
|
||||
if (sortBy === 'name') return (a.name || 'zzz').localeCompare(b.name || 'zzz');
|
||||
if (sortBy === 'seen') return (b.seen_count || 0) - (a.seen_count || 0);
|
||||
if (sortBy === 'distance') return (a.estimated_distance_m ?? 999) - (b.estimated_distance_m ?? 999);
|
||||
return 0;
|
||||
});
|
||||
sorted.forEach(device => renderDevice(device, false));
|
||||
applyDeviceFilter();
|
||||
highlightSelectedDevice(selectedDeviceId);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Device Rows
|
||||
|
||||
**`createSimpleDeviceCard(device)`** is rewritten to produce WiFi-style 2-line rows. All existing data attributes and filter attributes are preserved.
|
||||
|
||||
**Row HTML structure:**
|
||||
```html
|
||||
<div class="bt-device-row [is-tracker]"
|
||||
data-bt-device-id="…"
|
||||
data-is-new="…"
|
||||
data-has-name="…"
|
||||
data-rssi="…"
|
||||
data-is-tracker="…"
|
||||
data-search="…"
|
||||
role="button" tabindex="0" data-keyboard-activate="true"
|
||||
style="border-left-color: COLOR;">
|
||||
<div class="bt-row-top">
|
||||
<div class="bt-row-top-left">
|
||||
<span class="bt-proto-badge [ble|classic]">BLE|CLASSIC</span>
|
||||
<span class="bt-row-name [bt-unnamed]">NAME or ADDRESS</span>
|
||||
<!-- conditional badges: tracker, IRK, risk, flags, cluster -->
|
||||
</div>
|
||||
<div class="bt-row-top-right">
|
||||
<!-- conditional: flag badges -->
|
||||
<span class="bt-status-dot [new|known|tracker]"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bt-row-bottom">
|
||||
<div class="bt-signal-bar-wrap">
|
||||
<div class="bt-signal-track">
|
||||
<div class="bt-signal-fill [strong|medium|weak]" style="width: N%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bt-row-meta">
|
||||
<span>MANUFACTURER or —</span>
|
||||
<span>~Xm</span> <!-- omitted if no distance -->
|
||||
<span class="bt-row-rssi [strong|medium|weak]">−N</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Signal fill width:** `pct = rssi != null ? Math.max(0, Math.min(100, (rssi + 100) / 70 * 100)) : 0` (preserves existing BT formula — range is −100 to −30, not −100 to −20 as in WiFi).
|
||||
|
||||
**Signal fill class thresholds** (match existing `getRssiColor` breakpoints):
|
||||
- `.strong` (rssi ≥ −60): `background: linear-gradient(90deg, var(--accent-green), #88d49b)`
|
||||
- `.medium` (−60 > rssi ≥ −75): `background: linear-gradient(90deg, var(--accent-green), var(--accent-orange))`
|
||||
- `.weak` (rssi < −75): `background: linear-gradient(90deg, var(--accent-orange), var(--accent-red))`
|
||||
|
||||
**Left border colour:**
|
||||
- High-confidence tracker: `#ef4444`
|
||||
- Any tracker (lower confidence): `#f97316`
|
||||
- Non-tracker: result of `getRssiColor(rssi)` (unchanged)
|
||||
|
||||
**Unnamed device:** Address shown in `.bt-row-name.bt-unnamed` with `color: var(--text-dim); font-style: italic`.
|
||||
|
||||
**Badges moved to top-right (not top-left):** `PERSIST`, `BEACON`, `STABLE` flag badges move to `.bt-row-top-right` (before status dot), keeping top-left clean. Tracker, IRK, risk, and cluster badges remain in `.bt-row-top-left` after the name.
|
||||
|
||||
**Locate button:** Removed from the row entirely. It exists only in `#btDetailContent` (already present, no change needed).
|
||||
|
||||
**Selected row state:**
|
||||
```css
|
||||
.bt-device-row.selected {
|
||||
background: rgba(74, 163, 255, 0.07);
|
||||
border-left-color: var(--accent-cyan) !important;
|
||||
}
|
||||
```
|
||||
`highlightSelectedDevice()` is unchanged — it adds/removes `.selected` by `data-bt-device-id`.
|
||||
|
||||
### 4. Proximity Radar
|
||||
|
||||
**`proximity-radar.js` changes:**
|
||||
|
||||
`createSVG()` — add trailing arc group and CSS sweep class:
|
||||
- Replace `<line class="radar-sweep" …>` with a `<g class="bt-radar-sweep" clip-path="url(#radarClip)">` containing:
|
||||
- Two trailing arc `<path>` elements (90° and 60°) with low opacity fills
|
||||
- The sweep `<line>` element
|
||||
- Add `<clipPath id="radarClip"><circle …/></clipPath>` to `<defs>` to prevent arc overflow
|
||||
|
||||
`animateSweep()` — remove entirely. The CSS animation replaces it.
|
||||
|
||||
**CSS in `index.css` (BT section):**
|
||||
```css
|
||||
.bt-radar-sweep {
|
||||
transform-origin: CENTER_X px CENTER_Y px; /* 140px 140px — half of CONFIG.size */
|
||||
animation: bt-radar-rotate 3s linear infinite;
|
||||
}
|
||||
@keyframes bt-radar-rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
```
|
||||
|
||||
`isPaused` handling: when paused, add `animation-play-state: paused` to `.bt-radar-sweep` via a class toggle instead of the rAF check. `setPaused(paused)` updated:
|
||||
```js
|
||||
const sweep = svg?.querySelector('.bt-radar-sweep');
|
||||
if (sweep) sweep.style.animationPlayState = paused ? 'paused' : 'running';
|
||||
```
|
||||
|
||||
**Arc path geometry** (center = 140, outerRadius = center − padding = 120):
|
||||
- 90° arc endpoint: `(140 + 120, 140)` → `(260, 140)`
|
||||
- 60° arc endpoint: `x = 140 + 120·sin(60°) ≈ 244`, `y = 140 − 120·cos(60°) + 120 = 200` → `(244, 200)`
|
||||
- Sweep line: `x1=140 y1=140 x2=140 y2=20` (straight up)
|
||||
|
||||
## File Changes
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `templates/index.html` | BT device list header: add `#btScanIndicator`; insert new `.bt-controls-row` (sort group + filter group) between `.bt-list-signal-strip` and `.bt-device-toolbar`; remove old standalone `.bt-device-filters` div; `.bt-device-toolbar` (search input) is kept unchanged |
|
||||
| `static/js/modes/bluetooth.js` | Add `sortBy` state; add `renderAllDevices()`; rewrite `createSimpleDeviceCard()` for 2-line rows (no locate button); update `setScanning()` to drive `#btScanIndicator`; bind sort button listener in `init()`; remove the `locateBtn` branch from the delegated click handler in `bindListListeners()` (no `.bt-locate-btn[data-locate-id]` elements will exist in rows) |
|
||||
| `static/js/components/proximity-radar.js` | `createSVG()`: add clip path + trailing arc group + CSS class on sweep; remove `animateSweep()` function and its call; update `setPaused()` to use `animationPlayState` |
|
||||
| `static/css/index.css` | BT section: add `.bt-scan-indicator`, `.bt-scan-dot` + `@keyframes bt-scan-pulse`; replace `.bt-row-main`, `.bt-row-left/right`, `.bt-rssi-*`, `.bt-row-actions` with `.bt-row-top/bottom`, `.bt-signal-*`, `.bt-row-meta`, `.bt-row-name`; add `.bt-sort-btn`, `.bt-controls-row`, `.bt-sort-group`, `.bt-filter-group`; add `.bt-radar-sweep` + `@keyframes bt-radar-rotate` |
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Sidebar panel (scanner config, export, baseline)
|
||||
- Tracker detection panel (left column)
|
||||
- Zone summary cards under radar
|
||||
- Radar filter buttons (New Only / Strongest / Unapproved) and Pause button
|
||||
- Device detail panel (`#btDetailContent`) — already well-structured
|
||||
- Bluetooth locate mode
|
||||
- Any backend / route changes
|
||||
Reference in New Issue
Block a user