# LedGrab · Custom Card Icon · Proposal

01 Before / After CH · SIGNAL
WLED · OUT
Living Room Strip
192.168.1.42 · v0.14
PIXELS
144
LAT
8ms
CHIP
WS2812B
Bright
198
ONLINE
Before
WLED · OUT
Living Room Strip
192.168.1.42 · v0.14
PIXELS
144
LAT
8ms
CHIP
WS2812B
Bright
198
ONLINE
After · Strip
02 Anatomy Plate
52 × 52 · plate channel · tint edit pin · on hover corner · silkscreen

### One plate.
Five quiet signals.

The plate isn't a box around an icon. It's a small instrument-panel face that re-uses the card's channel color, the corner-bracket silkscreen, and the modular-rack scanline texture. Touch it and it lifts. Live cards make it breathe.

- Size
  52 × 52 px on cards · 40 × 40 on dashboard tiles · 34 × 34 on perf charts. Scales with the card's existing breakpoints — no new layout math.
- Tint
  Inherits `--ch` from the card (same variable that drives stripe, badge, fader). Pick a palette icon → it picks up the channel automatically.
- Override
  Optional per-card hex via the picker's color toggle. Stored as `device.icon_color`; falls back to `--ch` when null.
- States
  Idle · Running (breathing) · Offline (desaturated) · Fault (coral) · Empty (dashed placeholder).
- Storage
  Single field on the entity: `icon: "motherboard"`. Backwards-compatible — null hides the plate and the head reverts to today's badge-led layout.

03 States
IDLE · default
Resting state · channel-tinted · corner bracket visible · scanline texture.
RUNNING · live
2.6s breath syncs to the patch dot pulse · widens the stripe in lockstep.
OFFLINE
Glyph desaturates to `--lux-ink-mute` · plate keeps its outline so the slot stays anchored.
FAULT / EMPTY
Fault tints to coral. Empty state is dashed — a discoverable invitation, not a hole.
04 Coverage All Card Types
WLED · OUT
Couch Underglow
192.168.1.81 · v0.15
PIXELS
90
LAT
12ms
CHIP
SK6812
LIVE · OUT-2
OPENRGB · OUT
ROG Strix Z790-E
openrgb://localhost:6742/3
PIXELS
22
LAT
3ms
ZONE
AURA
STANDBY
OPENRGB · OUT
G502 Lightspeed
openrgb://localhost:6742/4
PIXELS
3
LAT
2ms
READY
OPENRGB · OUT
Keychron Q1 Pro
openrgb://localhost:6742/2
PIXELS
81
LAT
4ms
ZONE
PER-KEY
LIVE · OUT-3
HA · LIGHT
Hue Bedside Lamp
light.bedside_lamp · 2700K
STANDBY
SCREEN · IN
Primary Display
3440×1440 · DXGI · HDR
FPS
59.7
RES
3440
CPU
3%
CAPTURING
AUDIO · IN
Røde NT-USB Mini
48kHz · stereo · −12.4dB
Level
62
STREAMING
OPENRGB · OUT
RTX 4080 Aorus Master
openrgb://localhost:6742/1
PIXELS
12
TEMP
52°C
STANDBY
GAME · IN
Cyberpunk 2077
SDK · health · ammo · radio
WAITING
CLOCK · SYNC
Studio Tempo
120 BPM · 4/4 · master
NO ICON
WLED · OUT
Garage Strip
10.0.4.18 · timeout
OFFLINE · 2h 14m
AUDIO · IN
Studio Monitor L+R
loopback · 44.1kHz
STANDBY
05 Picker Modal · Open
Card · ROG Strix Z790-E

## Choose an icon

Tinted with the card's channel · click any tile to apply.
`⌘K`
Recent
Hardware
Lighting
Rooms & Furniture
Media
Signal
Ambience
`↵` Apply · `Esc` Cancel

## Implementation hints

- Schema: add `icon: str | None` and `icon_color: str | None` to the entity dataclass and Pydantic schema. Null on both → today's behaviour, no plate, no migration risk.
- Icon library: ship a curated subset of `lucide` as inline SVG `<symbol>`s in `icon-library.svg` — referenced by id (`<use href="#i-mouse"/>`). Tree-shaken at build time, no runtime import.
- Render slot: a new optional `head.icon: { id: string, color?: string }` on `ModHeadOpts`. `renderModHead` emits the plate before `.mod-id` when present and adds `mod-head--with-icon` for the head row alignment tweak.
- Picker entry points: ① click the plate on a card · ② new "Icon" row in `device-settings` modal · ③ `data-i18n` action in the kebab menu (“Change icon…”).
- Persistence: stored on the entity itself, not in a separate file — same JSON store, no migration needed for null values.
- Dashboard convergence: `.dashboard-target` already uses `.mod-head`, so the plate renders identically there. Smaller plate on `.perf-chart-card` via the `--plate-size` override.
- Search index: tag each icon with synonyms ("ssd → drive, disk, storage") so localisation lands cleanly through `t('icon.ssd.aliases')`.
- Keyboard: picker is `role="dialog"`; focus trap, `↵` applies, `Esc` cancels, `⌘K` jumps to search. Arrow-key grid navigation matches the existing `IconSelect` component.