hidden devices: blue ghosts, no live-state paint

A hidden device and an unavailable one both rendered as translucent dark —
indistinguishable at a glance, and a lit hidden lamp still glowed yellow
through the ghost (owner's report). A ghost is CONFIGURATION, not status:

- blue dashed ghost (accent-tinted, color-mix with an rgba fallback for old
  WebViews), clearly apart from the grey 'unavailable' icon;
- no state classes, no RGB tint, no alarm pulse, no active ripple on hidden
  devices — the only thing a ghost says is 'I am hidden, click to unhide'.

smoke_hidden_flag grew two assertions: the ghost carries no state classes
and is blue/dashed.
This commit is contained in:
Matysh
2026-07-29 11:49:37 +03:00
parent 694e1e9a3b
commit 2551b4ea0e
6 changed files with 59 additions and 28 deletions
+7
View File
@@ -39,6 +39,13 @@ Object.assign(out, await page.evaluate(async () => {
o.noGhostsInView = !sr().querySelector('.dev.ghost'); o.noGhostsInView = !sr().querySelector('.dev.ghost');
c._setMode('devices'); c.requestUpdate(); await c.updateComplete; c._setMode('devices'); c.requestUpdate(); await c.updateComplete;
o.ghostInEditor = !!sr().querySelector('.dev.ghost'); o.ghostInEditor = !!sr().querySelector('.dev.ghost');
// призрак — конфигурация, не статус: ни жёлтого, ни unavail, ни тревоги
const g = sr().querySelector('.dev.ghost');
o.ghostHasNoState = !!g && !g.classList.contains('on') && !g.classList.contains('open')
&& !g.classList.contains('unavail') && !g.classList.contains('alarm');
// и он синий, а не тёмный — отличим от недоступного устройства
o.ghostIsBlue = !!g && getComputedStyle(g).borderStyle.includes('dashed')
&& getComputedStyle(g).borderColor !== 'rgb(255, 255, 255)';
// тумблер локальный: конфиг не трогается // тумблер локальный: конфиг не трогается
o.toggleIsLocal = c._serverCfg.settings.show_all === undefined; o.toggleIsLocal = c._serverCfg.settings.show_all === undefined;
File diff suppressed because one or more lines are too long
+17 -10
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -242,7 +242,9 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
- [ ] Hide-from-plan flag (dev, docs/FILTERING.md): every device dialog has - [ ] Hide-from-plan flag (dev, docs/FILTERING.md): every device dialog has
the checkbox, incl. virtual; hidden devices vanish from every mode and the checkbox, incl. virtual; hidden devices vanish from every mode and
the count, still count toward room LQI, cast no glow/light fill; the the count, still count toward room LQI, cast no glow/light fill; the
device editor's "Show hidden" (local, per tab) shows them ghosted; device editor's "Show hidden" (local, per tab) shows them as BLUE
dashed ghosts — distinct from a grey unavailable icon — with NO live
state paint (no yellow, no alarm, no ripple);
unticking keeps a hidden:false marker (re-seed protection); an old unticking keeps a hidden:false marker (re-seed protection); an old
config materialises on first load by an editing client and legacy config materialises on first load by an editing client and legacy
clients keep the old behaviour until then clients keep the old behaviour until then
+5 -4
View File
@@ -4205,7 +4205,8 @@ class HouseplanCard extends LitElement {
const p = this._pos(d); const p = this._pos(d);
const left = ((p.x - view.x) / view.w) * 100; const left = ((p.x - view.x) / view.w) * 100;
const top = ((p.y - view.y) / view.h) * 100; const top = ((p.y - view.y) / view.h) * 100;
const cls = this._stateClass(d); // a ghost is configuration, not status: no yellow, no open, no unavail
const cls = d.hidden ? '' : this._stateClass(d);
const temp = this._liveTemp(d); const temp = this._liveTemp(d);
const hum = this._liveHum(d); const hum = this._liveHum(d);
const lqi = showLqi && !d.virtual ? lqiFor(this.hass, d.entities) : null; const lqi = showLqi && !d.virtual ? lqiFor(this.hass, d.entities) : null;
@@ -4229,15 +4230,15 @@ class HouseplanCard extends LitElement {
// RGB lights color the icon (and the ripple, unless a custom ripple color is set); // RGB lights color the icon (and the ripple, unless a custom ripple color is set);
// an icon with controlled targets takes the color of its first lit RGB target // an icon with controlled targets takes the color of its first lit RGB target
const ctrl = (m?.controls || []).filter(isControllable); const ctrl = (m?.controls || []).filter(isControllable);
const lightC = this._config?.live_states const lightC = this._config?.live_states && !d.hidden
? ctrl.length ? ctrl.length
? ctrl.map((e) => lightColorOf(this.hass.states[e])).find((v) => v) || null ? ctrl.map((e) => lightColorOf(this.hass.states[e])).find((v) => v) || null
: domain === 'light' ? lightColorOf(primarySt) : null : domain === 'light' ? lightColorOf(primarySt) : null
: null; : null;
// emergencies (leak/smoke/gas/CO/siren) pulse red regardless of display mode // emergencies (leak/smoke/gas/CO/siren) pulse red regardless of display mode
const alarm = this._config?.live_states const alarm = this._config?.live_states && !d.hidden
&& isAlarmState(domain, primarySt?.attributes?.device_class, primarySt?.state); && isAlarmState(domain, primarySt?.attributes?.device_class, primarySt?.state);
const active = ripple && !!d.primary && isActiveState(this.hass.states[d.primary]?.state); const active = ripple && !d.hidden && !!d.primary && isActiveState(this.hass.states[d.primary]?.state);
const scale = Number(m?.size) > 0 ? Number(m!.size) : 1; const scale = Number(m?.size) > 0 ? Number(m!.size) : 1;
const angle = Number(m?.angle) || 0; const angle = Number(m?.angle) || 0;
const rScale = Number(m?.ripple_size) > 0 ? Number(m!.ripple_size) : 3; const rScale = Number(m?.ripple_size) > 0 ? Number(m!.ripple_size) : 3;
+10 -3
View File
@@ -964,11 +964,18 @@ export const cardStyles = css`
border-style: dashed; border-style: dashed;
} }
/* "hide from plan" flag, shown only in the device editor with the /* "hide from plan" flag, shown only in the device editor with the
"show hidden devices" toggle on (docs/FILTERING.md) */ "show hidden devices" toggle on (docs/FILTERING.md). BLUE, so a hidden
device cannot be mistaken for an unavailable one (translucent dark)
and no live-state paint at all: a ghost is configuration, not status
(owner's request). */
.dev.ghost { .dev.ghost {
opacity: 0.4; opacity: 0.6;
border-style: dashed; border-style: dashed;
filter: saturate(0.4); border-color: var(--hp-accent);
background: rgba(62, 166, 255, 0.22); /* fallback for old WebViews */
background: color-mix(in srgb, var(--hp-accent) 30%, var(--card-background-color, #1c2530));
color: var(--hp-accent);
box-shadow: none;
} }
.dev.sel { .dev.sel {
border-color: #ffc14d; border-color: #ffc14d;