mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 23:19:14 +00:00
+51
-18
@@ -18,6 +18,13 @@ export interface LegacySupplementalMetric {
|
||||
suffix: string;
|
||||
}
|
||||
|
||||
/** HA theme state is authoritative when available; CSS color-scheme remains the fallback. */
|
||||
export function deviceThemeClass(hass: any): string {
|
||||
return typeof hass?.themes?.darkMode === 'boolean'
|
||||
? hass.themes.darkMode ? 'theme-dark' : 'theme-light'
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue #90 replaced the automatic temperature/humidity satellites with one
|
||||
* configurable value badge. Untouched legacy markers, however, could show
|
||||
@@ -62,6 +69,16 @@ export function lqiClassName(badge: ResolvedValueBadge | null | undefined): stri
|
||||
return `lqi${badge?.position === 'bottom' ? ' below-value-badge' : ''}`;
|
||||
}
|
||||
|
||||
/** Deterministic fit without DOM measurement or a per-marker ResizeObserver. */
|
||||
export function deviceTextScale(text: string): number {
|
||||
const units = [...String(text)].reduce((sum, char) => {
|
||||
if (/\s/.test(char)) return sum + 0.35;
|
||||
return sum + (char.codePointAt(0)! > 0xff ? 1 : 0.62);
|
||||
}, 0);
|
||||
if (units <= 8) return 0.45;
|
||||
return Math.max(0.25, Math.round((0.45 * 8 / units) * 1000) / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render only the face contents. The owning surface keeps coordinates,
|
||||
* pointer handlers, tooltip and selection semantics; this fragment is shared.
|
||||
@@ -73,6 +90,15 @@ export function renderDeviceFace(
|
||||
const pulse = presentation.pulse;
|
||||
const gen2 = pulse.generation % 2 === 0;
|
||||
const legacyMetrics = legacySupplementalMetrics(presentation);
|
||||
const badge = presentation.valueBadge;
|
||||
const shellPosition = badge?.position || 'right';
|
||||
const hasSections = !!badge || legacyMetrics.length > 0;
|
||||
const shellClasses = [
|
||||
'device-shell',
|
||||
presentation.valueText != null ? 'text-shell' : '',
|
||||
hasSections ? `with-values pos-${shellPosition}` : '',
|
||||
legacyMetrics.length ? 'with-legacy' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
return html`
|
||||
${pulse.kind !== 'none' && pulse.reducedMotionIndicator !== 'dot'
|
||||
? html`<span class="device-pulse activity-ring ${pulse.kind} ${pulse.reason} reason-${pulse.reason} ${gen2 ? 'gen2' : ''}"
|
||||
@@ -87,25 +113,32 @@ export function renderDeviceFace(
|
||||
${presentation.haDisabled
|
||||
? html`<span class="habadge" title=${options.disabledTitle || ''} aria-hidden="true"><ha-icon icon="mdi:power-plug-off-outline"></ha-icon></span>`
|
||||
: nothing}
|
||||
${presentation.valueText != null
|
||||
? html`<span class="valtext" title=${presentation.valueFullText || presentation.valueText}
|
||||
aria-label=${presentation.valueFullText || presentation.valueText}>${presentation.valueText}</span>`
|
||||
: html`<ha-icon icon=${presentation.icon}
|
||||
style=${presentation.angle ? `transform:rotate(${presentation.angle}deg)` : nothing}></ha-icon>`}
|
||||
${presentation.valueBadge
|
||||
? html`<span
|
||||
class=${valueBadgeClassName(presentation.valueBadge)}
|
||||
title=${valueBadgeTitle(presentation.valueBadge)}
|
||||
aria-hidden="true"
|
||||
>${presentation.valueBadge.text}</span>`
|
||||
: nothing}
|
||||
${legacyMetrics.map((metric) => html`<span
|
||||
class="value-badge legacy-secondary pos-right available tone-${metric.kind}"
|
||||
title=${metric.text + metric.suffix}
|
||||
aria-hidden="true"
|
||||
>${metric.text}${metric.suffix}</span>`)}
|
||||
<span class=${shellClasses} aria-hidden="true">
|
||||
<span class="device-core">
|
||||
${presentation.valueText != null
|
||||
? html`<span class="valtext" title=${presentation.valueFullText || presentation.valueText}
|
||||
style=${`--value-font-scale:${deviceTextScale(presentation.valueFullText || presentation.valueText)}`}
|
||||
>${presentation.valueText}</span>`
|
||||
: html`<ha-icon icon=${presentation.icon}
|
||||
style=${presentation.angle ? `transform:rotate(${presentation.angle}deg)` : nothing}></ha-icon>`}
|
||||
</span>
|
||||
${hasSections ? html`<span class="device-sections">
|
||||
${badge
|
||||
? html`<span
|
||||
class=${valueBadgeClassName(badge)}
|
||||
title=${valueBadgeTitle(badge)}
|
||||
style=${`--value-font-scale:${deviceTextScale(badge.fullText || badge.text)}`}
|
||||
>${badge.text}</span>`
|
||||
: nothing}
|
||||
${legacyMetrics.map((metric) => html`<span
|
||||
class="value-badge legacy-secondary available tone-${metric.kind}"
|
||||
title=${metric.text + metric.suffix}
|
||||
style=${`--value-font-scale:${deviceTextScale(metric.text + metric.suffix)}`}
|
||||
>${metric.text}${metric.suffix}</span>`)}
|
||||
</span>` : nothing}
|
||||
</span>
|
||||
${presentation.lqiText != null
|
||||
? html`<span class=${lqiClassName(presentation.valueBadge)}
|
||||
? html`<span class="${lqiClassName(presentation.valueBadge)}${presentation.lqiBand ? ` band-${presentation.lqiBand}` : ''}"
|
||||
style=${presentation.lqiColor ? `color:${presentation.lqiColor}` : nothing}>${presentation.lqiText}</span>`
|
||||
: nothing}
|
||||
`;
|
||||
|
||||
@@ -16,14 +16,16 @@ import {
|
||||
type DeviceActivity, type DeviceVisualState, type EntityVisualSample,
|
||||
} from './device-visual';
|
||||
import {
|
||||
hassValue, lightColorOf, lqiColor, normalizeDeviceDisplay, stateIcon, valueWithUnit,
|
||||
hassValue, lightColorOf, normalizeDeviceDisplay, stateIcon, valueWithUnit,
|
||||
type DeviceDisplayMode,
|
||||
} from './logic';
|
||||
import { resolveToggleIntent, toggleCoverEntity } from './device-toggle';
|
||||
import type { DevItem } from './types';
|
||||
import { safeStoredColor } from './color';
|
||||
import { resolveDeviceValueBadge, type ResolvedValueBadge } from './device-value-badge';
|
||||
import { resolveDevicePulse, type ResolvedDevicePulse } from './device-pulse';
|
||||
import {
|
||||
DEVICE_PULSE_DEFAULT_SCALE, resolveDevicePulse, type ResolvedDevicePulse,
|
||||
} from './device-pulse';
|
||||
import { isManualVirtualLightMarker } from './virtual-light-state';
|
||||
|
||||
export type PresentationSourceKind =
|
||||
@@ -36,6 +38,35 @@ export type PresentationSourceRole =
|
||||
export type ValueFallbackReason =
|
||||
| 'value_no_state' | 'value_ambiguous_sources' | 'value_non_scalar' | 'value_virtual';
|
||||
|
||||
export type DeviceLockState = 'locked' | 'unlocked' | null;
|
||||
export type DeviceLqiBand = 'low' | 'mid' | 'high';
|
||||
export type DeviceA11yState =
|
||||
| 'neutral' | 'working' | 'open' | 'locked' | 'unlocked' | 'alarm' | 'unavailable';
|
||||
|
||||
export function markerLqiBand(lqi: number): DeviceLqiBand {
|
||||
if (lqi <= 40) return 'low';
|
||||
if (lqi < 180) return 'mid';
|
||||
return 'high';
|
||||
}
|
||||
|
||||
/** Marker numbers are categorical. Room fill deliberately keeps logic.ts's gradient. */
|
||||
export function markerLqiColor(lqi: number): string {
|
||||
const band = markerLqiBand(lqi);
|
||||
return band === 'low' ? '#F0410C' : band === 'mid' ? '#F0A00C' : '#1DC21D';
|
||||
}
|
||||
|
||||
export function deviceA11yState(presentation: Pick<
|
||||
ResolvedDevicePresentation, 'visual' | 'lockState' | 'display'
|
||||
>): DeviceA11yState {
|
||||
if (presentation.display === 'static_icon') return 'neutral';
|
||||
if (presentation.visual.status === 'alarm') return 'alarm';
|
||||
if (presentation.visual.availability === 'unavailable') return 'unavailable';
|
||||
if (presentation.lockState) return presentation.lockState;
|
||||
if (presentation.visual.status === 'working') return 'working';
|
||||
if (presentation.visual.status === 'open') return 'open';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
export type PresentationReason =
|
||||
| 'neutral'
|
||||
| 'working'
|
||||
@@ -123,6 +154,7 @@ export interface ResolvedDevicePresentation {
|
||||
sourceSignature: string;
|
||||
|
||||
visual: DeviceVisualState;
|
||||
lockState: DeviceLockState;
|
||||
display: DeviceDisplayMode;
|
||||
icon: string;
|
||||
valueText: string | null;
|
||||
@@ -138,6 +170,7 @@ export interface ResolvedDevicePresentation {
|
||||
valueBadge: ResolvedValueBadge | null;
|
||||
lqiText: string | null;
|
||||
lqiColor: string | null;
|
||||
lqiBand: DeviceLqiBand | null;
|
||||
lightColor: string | null;
|
||||
scale: number;
|
||||
angle: number;
|
||||
@@ -522,7 +555,7 @@ function explanationReason(
|
||||
|
||||
export function presentationClasses(presentation: Pick<
|
||||
ResolvedDevicePresentation,
|
||||
'visual' | 'activity' | 'display' | 'effectiveHidden' | 'activityGeneration' | 'pulse'
|
||||
'visual' | 'lockState' | 'activity' | 'display' | 'effectiveHidden' | 'activityGeneration' | 'pulse'
|
||||
>): string[] {
|
||||
if (presentation.effectiveHidden) return [];
|
||||
if (presentation.display === 'static_icon') return ['static-icon'];
|
||||
@@ -532,6 +565,7 @@ export function presentationClasses(presentation: Pick<
|
||||
else if (visual.availability === 'unavailable') classes.push('unavail');
|
||||
else if (visual.status === 'working') classes.push('on');
|
||||
else if (visual.status === 'open') classes.push('open');
|
||||
if (presentation.lockState) classes.push(`lock-${presentation.lockState}`);
|
||||
// Keep the semantic class names as styling/debug hooks during the beta;
|
||||
// they no longer select a separate renderer or animation implementation.
|
||||
if (presentation.pulse.reason !== 'none' && presentation.pulse.reason !== 'alarm') {
|
||||
@@ -563,6 +597,12 @@ export function resolveDevicePresentation(
|
||||
const userHidden = d.userHidden === true || d.marker?.hidden === true;
|
||||
const effectiveHidden = haDisabled || (userHidden && !options.designPreview);
|
||||
const combined = combineVisualSamples(sources.samples);
|
||||
const lockSource = sources.visualSources.find((source) => source.eid.startsWith('lock.'));
|
||||
const lockState: DeviceLockState = lockSource
|
||||
? ['unlocked', 'open'].includes(lockSource.state.toLowerCase())
|
||||
? 'unlocked'
|
||||
: lockSource.state.toLowerCase() === 'locked' ? 'locked' : null
|
||||
: null;
|
||||
let visual = combined;
|
||||
if (effectiveHidden) visual = { availability: 'available', status: 'neutral', activity: 'none' };
|
||||
else if (staticIcon) visual = { availability: 'available', status: 'neutral', activity: 'none' };
|
||||
@@ -626,7 +666,8 @@ export function resolveDevicePresentation(
|
||||
: null;
|
||||
const scale = Number(d.marker?.size) > 0 ? Number(d.marker!.size) : 1;
|
||||
const angle = Number(d.marker?.angle) || 0;
|
||||
const rippleScale = Number(d.marker?.ripple_size) > 0 ? Number(d.marker!.ripple_size) : 3;
|
||||
const rippleScale = Number(d.marker?.ripple_size) > 0
|
||||
? Number(d.marker!.ripple_size) : DEVICE_PULSE_DEFAULT_SCALE;
|
||||
const configuredRippleColor = safeStoredColor(d.marker?.ripple_color, null);
|
||||
const rippleColor = staticIcon ? null : configuredRippleColor || lightColor || null;
|
||||
const pulse = resolveDevicePulse({
|
||||
@@ -673,6 +714,7 @@ export function resolveDevicePresentation(
|
||||
valueSource: value.source,
|
||||
sourceSignature,
|
||||
visual,
|
||||
lockState,
|
||||
display,
|
||||
icon,
|
||||
valueText,
|
||||
@@ -686,7 +728,8 @@ export function resolveDevicePresentation(
|
||||
humText: hum == null ? null : String(hum),
|
||||
valueBadge,
|
||||
lqiText: lqi == null || valueBadge?.isLqi ? null : String(lqi),
|
||||
lqiColor: lqi == null ? null : lqiColor(lqi),
|
||||
lqiColor: lqi == null ? null : markerLqiColor(lqi),
|
||||
lqiBand: lqi == null ? null : markerLqiBand(lqi),
|
||||
lightColor,
|
||||
scale,
|
||||
angle,
|
||||
|
||||
+24
-8
@@ -31,8 +31,24 @@ export interface ResolveDevicePulseOptions {
|
||||
diameterScale?: number;
|
||||
}
|
||||
|
||||
const ALARM_COLOR = '#f25a4a';
|
||||
const ALARM_DIAMETER_SCALE = 3;
|
||||
export const DEVICE_ACTIVITY_BLUE = '#0C82F0';
|
||||
export const DEVICE_ACTIVITY_AMBER = '#F0A00C';
|
||||
export const DEVICE_ACTIVITY_GREEN = '#1DC21D';
|
||||
export const DEVICE_ALERT_RED = '#F0410C';
|
||||
export const DEVICE_PULSE_DEFAULT_SCALE = 1.5;
|
||||
|
||||
const ALARM_DIAMETER_SCALE = DEVICE_PULSE_DEFAULT_SCALE;
|
||||
|
||||
function semanticPulseColor(
|
||||
visual: DeviceVisualState,
|
||||
semanticActivity: DeviceActivity,
|
||||
): string {
|
||||
if (semanticActivity === 'presence') return DEVICE_ACTIVITY_GREEN;
|
||||
if (semanticActivity === 'running' || visual.status === 'working' || visual.status === 'open') {
|
||||
return DEVICE_ACTIVITY_AMBER;
|
||||
}
|
||||
return DEVICE_ACTIVITY_BLUE;
|
||||
}
|
||||
|
||||
function noPulse(
|
||||
generation: number,
|
||||
@@ -60,8 +76,8 @@ export function resolveDevicePulse(options: ResolveDevicePulseOptions): Resolved
|
||||
} = options;
|
||||
const generation = Math.max(1, Math.trunc(options.shortGeneration || 1));
|
||||
const diameterScale = Number.isFinite(options.diameterScale)
|
||||
? Math.max(1, Number(options.diameterScale)) : 3;
|
||||
const color = options.color || null;
|
||||
? Math.max(1, Number(options.diameterScale)) : DEVICE_PULSE_DEFAULT_SCALE;
|
||||
const color = options.color || semanticPulseColor(visual, semanticActivity);
|
||||
if (effectiveHidden || bindingUnavailable || visual.availability === 'unavailable'
|
||||
|| display === 'static_icon') {
|
||||
return noPulse(generation, color, diameterScale);
|
||||
@@ -72,7 +88,7 @@ export function resolveDevicePulse(options: ResolveDevicePulseOptions): Resolved
|
||||
if (visual.status === 'alarm') {
|
||||
return {
|
||||
kind: 'alarm', reason: 'alarm', generation, expiresAt: null,
|
||||
color: ALARM_COLOR, diameterScale: ALARM_DIAMETER_SCALE, animated: !reducedMotion,
|
||||
color: DEVICE_ALERT_RED, diameterScale: ALARM_DIAMETER_SCALE, animated: !reducedMotion,
|
||||
reducedMotionIndicator: 'none',
|
||||
};
|
||||
}
|
||||
@@ -83,7 +99,7 @@ export function resolveDevicePulse(options: ResolveDevicePulseOptions): Resolved
|
||||
if (options.shortReason && expiresAt != null && expiresAt > now) {
|
||||
return {
|
||||
kind: 'short', reason: options.shortReason, generation, expiresAt,
|
||||
color: reducedMotion ? null : color,
|
||||
color,
|
||||
diameterScale: reducedMotion ? 1 : diameterScale,
|
||||
animated: !reducedMotion,
|
||||
reducedMotionIndicator: reducedMotion ? 'dot' : 'none',
|
||||
@@ -95,7 +111,7 @@ export function resolveDevicePulse(options: ResolveDevicePulseOptions): Resolved
|
||||
|| semanticActivity === 'running') {
|
||||
return {
|
||||
kind: 'continuous', reason: semanticActivity, generation, expiresAt: null,
|
||||
color: reducedMotion ? null : color,
|
||||
color,
|
||||
diameterScale: reducedMotion ? 1 : diameterScale,
|
||||
animated: !reducedMotion,
|
||||
reducedMotionIndicator: reducedMotion ? 'dot' : 'none',
|
||||
@@ -117,7 +133,7 @@ export function withDemoPulse(
|
||||
reason: kind === 'short' ? 'event' : 'running',
|
||||
generation: Math.max(1, Math.trunc(generation)),
|
||||
expiresAt: kind === 'short' ? expiresAt : null,
|
||||
color: reducedMotion ? null : source.color,
|
||||
color: source.color,
|
||||
diameterScale: reducedMotion ? 1 : source.diameterScale,
|
||||
animated: !reducedMotion,
|
||||
reducedMotionIndicator: reducedMotion ? 'dot' : 'none',
|
||||
|
||||
+31
-10
@@ -196,7 +196,7 @@ import {
|
||||
type DeviceActivity, type DeviceVisualState, type EntityVisualSample,
|
||||
} from './device-visual';
|
||||
import {
|
||||
activitySourceSignature, presentationClasses, resolveDevicePresentation,
|
||||
activitySourceSignature, deviceA11yState, presentationClasses, resolveDevicePresentation,
|
||||
resolvePresentationSources, type ResolvedDevicePresentation,
|
||||
} from './device-presentation';
|
||||
import {
|
||||
@@ -211,7 +211,7 @@ import {
|
||||
createRenderDeviceSnapshot, presentationSnapshotKey, renderDeviceSnapshotPositions,
|
||||
type RenderDeviceSnapshot,
|
||||
} from './render-device-snapshot';
|
||||
import { deviceFaceStyle, renderDeviceFace } from './device-face';
|
||||
import { deviceFaceStyle, deviceThemeClass, renderDeviceFace } from './device-face';
|
||||
import {
|
||||
ModeTransitionController, viewportFromViewBox,
|
||||
type HouseplanMode, type ModeTransitionState, type ModeVisualState, type ModeViewBox,
|
||||
@@ -1527,7 +1527,7 @@ class HouseplanCard extends LitElement {
|
||||
autoIcon: string; // the icon the rules would give — picker placeholder
|
||||
display: DeviceDisplayMode;
|
||||
rippleColor: string; // '' = accent
|
||||
rippleSize: number; // in icon diameters
|
||||
rippleSize: number; // in icon diameters; package default 1.5
|
||||
size: number; // icon size multiplier
|
||||
angle: number; // icon rotation, degrees
|
||||
/** UI projection. A legacy persisted `cover` is shown as `toggle`. */
|
||||
@@ -4528,7 +4528,7 @@ class HouseplanCard extends LitElement {
|
||||
else this._infoCard = d;
|
||||
}
|
||||
|
||||
private _clickDevice(ev: MouseEvent, d: DevItem): void {
|
||||
private _clickDevice(ev: Event, d: DevItem): void {
|
||||
ev.stopPropagation();
|
||||
if (this._drag?.moved || this._suppressClick || this._holdFired) return;
|
||||
if (this._mode === 'plan') return;
|
||||
@@ -4644,6 +4644,13 @@ class HouseplanCard extends LitElement {
|
||||
this._infoCard = actionDevice;
|
||||
}
|
||||
|
||||
private _keyDevice(ev: KeyboardEvent, d: DevItem): void {
|
||||
if (ev.key !== 'Enter' && ev.key !== ' ') return;
|
||||
if (this._mode !== 'view' && this._mode !== 'devices') return;
|
||||
ev.preventDefault();
|
||||
this._clickDevice(ev, d);
|
||||
}
|
||||
|
||||
/** Translate a key in the card's current language. */
|
||||
private _t(key: I18nKey, vars?: Record<string, string | number>): string {
|
||||
return t(langOf(this.hass, this._config?.language), key, vars);
|
||||
@@ -12446,7 +12453,7 @@ class HouseplanCard extends LitElement {
|
||||
autoIcon: d.icon || '',
|
||||
display: normalizeDeviceDisplay(d.marker?.display),
|
||||
rippleColor: safeStoredColor(d.marker?.ripple_color, ''),
|
||||
rippleSize: Number(d.marker?.ripple_size) > 0 ? Number(d.marker!.ripple_size) : 3,
|
||||
rippleSize: Number(d.marker?.ripple_size) > 0 ? Number(d.marker!.ripple_size) : 1.5,
|
||||
size: Number(d.marker?.size) > 0 ? Number(d.marker!.size) : 1,
|
||||
angle: Number(d.marker?.angle) || 0,
|
||||
tapAction: projectedTapAction(d.marker?.tap_action, d.primary?.split('.')[0]),
|
||||
@@ -12508,7 +12515,7 @@ class HouseplanCard extends LitElement {
|
||||
this._markerDialog = {
|
||||
name: '', binding: 'virtual', bindingMode: 'virtual', bindingOpen: false,
|
||||
showEntities: false, bindingFilter: '', icon: '', autoIcon: '',
|
||||
display: 'badge', rippleColor: '', rippleSize: 3, size: 1, angle: 0,
|
||||
display: 'badge', rippleColor: '', rippleSize: 1.5, size: 1, angle: 0,
|
||||
tapAction: 'info', tapActionTouched: false,
|
||||
originalHasTapAction: false, originalTapAction: undefined, tapHintAnnouncement: '',
|
||||
toggleEntity: '', toggleEntityTouched: false,
|
||||
@@ -12997,7 +13004,7 @@ class HouseplanCard extends LitElement {
|
||||
icon: dlg.icon || null,
|
||||
display: dlg.display !== 'badge' ? dlg.display : null,
|
||||
ripple_color: dlg.display === 'icon_ripple' && dlg.rippleColor ? dlg.rippleColor : null,
|
||||
ripple_size: dlg.display === 'icon_ripple' && dlg.rippleSize !== 3 ? dlg.rippleSize : null,
|
||||
ripple_size: dlg.display === 'icon_ripple' && dlg.rippleSize !== 1.5 ? dlg.rippleSize : null,
|
||||
size: dlg.size !== 1 ? dlg.size : null,
|
||||
angle: dlg.angle ? dlg.angle : null,
|
||||
...this._markerTapActionFields(dlg),
|
||||
@@ -16740,11 +16747,20 @@ class HouseplanCard extends LitElement {
|
||||
const ghostLabel = presentation.haDisabled
|
||||
? this._t((`marker.ha_disabled_${disabledReason}`) as any)
|
||||
: d.userHidden ? this._t('marker.hidden_ghost') : d.name;
|
||||
const a11yState = deviceA11yState(presentation);
|
||||
const interactive = this._mode === 'view' || this._mode === 'devices';
|
||||
const deviceAriaLabel = [
|
||||
ghostLabel,
|
||||
!presentation.haDisabled
|
||||
? this._t((`marker.state_a11y_${a11yState}`) as I18nKey) : '',
|
||||
presentation.pulse.kind !== 'none'
|
||||
? this._t((`marker.pulse_a11y_${presentation.pulse.reason}`) as I18nKey) : '',
|
||||
presentation.valueFullText || presentation.valueText || '',
|
||||
valueBadgeTitle(presentation.valueBadge),
|
||||
presentation.lqiText != null && presentation.lqiBand
|
||||
? this._t((`marker.lqi_a11y_${presentation.lqiBand}`) as I18nKey, {
|
||||
value: presentation.lqiText,
|
||||
}) : '',
|
||||
].filter(Boolean).join(', ');
|
||||
const metrics = [
|
||||
d.model,
|
||||
@@ -16762,10 +16778,15 @@ class HouseplanCard extends LitElement {
|
||||
data-area=${d.area || nothing}
|
||||
data-binding-status=${presentation.haDisabled ? 'ha-disabled' : d.bindingStatus?.kind || 'active'}
|
||||
data-disabled-reason=${disabledReason ? disabledReason.replace('_', '-') : nothing}
|
||||
data-state=${a11yState}
|
||||
data-lqi-band=${presentation.lqiText != null ? presentation.lqiBand || nothing : nothing}
|
||||
role=${interactive ? 'button' : nothing}
|
||||
tabindex=${interactive ? '0' : nothing}
|
||||
aria-label=${deviceAriaLabel}
|
||||
class="dev ${presentation.classes.join(' ')} ${this._selId === d.id ? 'sel' : ''} ${d.virtual ? 'virtual' : ''} ${d.hidden ? 'ghost' : ''} ${presentation.haDisabled ? 'ha-disabled' : ''} ${presentation.valueText != null ? 'valonly' : ''}"
|
||||
class="dev ${deviceThemeClass(this._renderPlanHass)} ${presentation.classes.join(' ')} ${this._selId === d.id ? 'sel' : ''} ${d.virtual ? 'virtual' : ''} ${d.hidden ? 'ghost' : ''} ${presentation.haDisabled ? 'ha-disabled' : ''} ${presentation.valueText != null ? 'valonly' : ''}"
|
||||
style="${st.join(';')}"
|
||||
@click=${(e: MouseEvent) => this._clickDevice(e, d)}
|
||||
@keydown=${(e: KeyboardEvent) => this._keyDevice(e, d)}
|
||||
@contextmenu=${(e: MouseEvent) => this._ctxDevice(e, d)}
|
||||
@mousemove=${(e: MouseEvent) =>
|
||||
this._showTip(e, d.name, presentation.haDisabled ? ghostLabel : metrics)}
|
||||
@@ -18445,7 +18466,7 @@ class HouseplanCard extends LitElement {
|
||||
icon: d.icon || null,
|
||||
display: d.display !== 'badge' ? d.display : null,
|
||||
ripple_color: d.display === 'icon_ripple' && d.rippleColor ? d.rippleColor : null,
|
||||
ripple_size: d.display === 'icon_ripple' && d.rippleSize !== 3 ? d.rippleSize : null,
|
||||
ripple_size: d.display === 'icon_ripple' && d.rippleSize !== 1.5 ? d.rippleSize : null,
|
||||
size: d.size !== 1 ? d.size : null,
|
||||
angle: d.angle || null,
|
||||
...this._markerTapActionFields(d),
|
||||
@@ -19416,7 +19437,7 @@ class HouseplanCard extends LitElement {
|
||||
</div>
|
||||
<div class="colorrow ripple-sizerow">
|
||||
<span class="opl">${this._t('marker.ripple_size')}</span>
|
||||
${this._rangeInput(2, 8, 0.5, d.rippleSize, (n) => (this._markerDialog = { ...d, rippleSize: n }))}
|
||||
${this._rangeInput(1, 8, 0.5, d.rippleSize, (n) => (this._markerDialog = { ...d, rippleSize: n }))}
|
||||
<span class="opv">×${d.rippleSize}</span>
|
||||
</div>
|
||||
<p class="muted" role="note">${this._t('marker.activity_alarm_note')}</p>`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Live, read-only device display preview used by the marker editor. */
|
||||
import { LitElement, css, html, nothing, type PropertyValues, type TemplateResult } from 'lit';
|
||||
import { cardStyles } from './styles';
|
||||
import { deviceFaceStyle, renderDeviceFace } from './device-face';
|
||||
import { deviceFaceStyle, deviceThemeClass, renderDeviceFace } from './device-face';
|
||||
import {
|
||||
type PresentationReason,
|
||||
type ResolvedDevicePresentation,
|
||||
@@ -231,7 +231,7 @@ class HpDevicePreview extends LitElement {
|
||||
fit < 1 ? `transform:scale(${fit})` : '',
|
||||
].filter(Boolean).join(';');
|
||||
const rootClasses = [
|
||||
'dev', ...shown.classes,
|
||||
'dev', deviceThemeClass(this.hass), ...shown.classes,
|
||||
shown.binding === 'virtual' ? 'virtual' : '',
|
||||
shown.haDisabled ? 'ghost ha-disabled' : '',
|
||||
shown.valueText != null ? 'valonly' : '',
|
||||
@@ -248,6 +248,9 @@ class HpDevicePreview extends LitElement {
|
||||
result,
|
||||
actual.valueFullText || '',
|
||||
valueBadgeTitle(actual.valueBadge),
|
||||
actual.lqiText != null && actual.lqiBand
|
||||
? this._t((`marker.lqi_a11y_${actual.lqiBand}`) as any, { value: actual.lqiText })
|
||||
: '',
|
||||
].filter(Boolean).join('. ');
|
||||
|
||||
return html`<section class="devicepreview">
|
||||
|
||||
+11
-1
@@ -176,7 +176,7 @@
|
||||
"marker.display_hint_icon": "The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.",
|
||||
"marker.display_hint_icon_ripple": "The icon, dynamic plate and pulse show a short pulse for events and a continuous pulse for work, motion or presence. Red alarms have separate priority.",
|
||||
"marker.display_hint_value": "One unambiguous Home Assistant value replaces the icon while the plate continues to show state. Red alarms remain visible.",
|
||||
"marker.display_hint_static_icon": "The black plate and icon always stay the same. State, activity, unavailability and alarms do not change the face.",
|
||||
"marker.display_hint_static_icon": "The theme-aware shell and icon always stay the same. State, activity, unavailability and alarms do not change the face.",
|
||||
"marker.static_alarm_warning": "This device can report alarms. Static display hides the marker's visual alarm indication.",
|
||||
"marker.preview.title": "Display preview",
|
||||
"marker.preview.actual": "Now",
|
||||
@@ -235,6 +235,16 @@
|
||||
"marker.pulse_a11y_presence": "Presence detected",
|
||||
"marker.pulse_a11y_transition": "Changing state",
|
||||
"marker.pulse_a11y_running": "Working",
|
||||
"marker.state_a11y_neutral": "Default state",
|
||||
"marker.state_a11y_working": "Working",
|
||||
"marker.state_a11y_open": "Open",
|
||||
"marker.state_a11y_locked": "Locked",
|
||||
"marker.state_a11y_unlocked": "Unlocked",
|
||||
"marker.state_a11y_alarm": "Alarm",
|
||||
"marker.state_a11y_unavailable": "Unavailable",
|
||||
"marker.lqi_a11y_low": "LQI {value}, low signal",
|
||||
"marker.lqi_a11y_mid": "LQI {value}, medium signal",
|
||||
"marker.lqi_a11y_high": "LQI {value}, high signal",
|
||||
"marker.size_label": "Icon size / rotation",
|
||||
"marker.angle_label": "Rotate",
|
||||
"marker.model_label": "Model",
|
||||
|
||||
+11
-1
@@ -176,7 +176,7 @@
|
||||
"marker.display_hint_icon": "Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.",
|
||||
"marker.display_hint_icon_ripple": "Значок, динамическая подложка и пульсация: короткая — для событий, постоянная — для работы, движения или присутствия. Красная тревога имеет отдельный приоритет.",
|
||||
"marker.display_hint_value": "Значок заменяется одним однозначным значением Home Assistant; подложка продолжает показывать состояние. Красная тревога сохраняется.",
|
||||
"marker.display_hint_static_icon": "Всегда одинаковые чёрная подложка и значок. Состояния, активность, недоступность и тревоги не меняют отображение.",
|
||||
"marker.display_hint_static_icon": "Подложка в цветах темы и значок всегда остаются одинаковыми. Состояния, активность, недоступность и тревоги не меняют отображение.",
|
||||
"marker.static_alarm_warning": "Это устройство может сообщать о тревогах. Статичный режим скрывает визуальную тревогу маркера.",
|
||||
"marker.preview.title": "Предпросмотр отображения",
|
||||
"marker.preview.actual": "Сейчас",
|
||||
@@ -235,6 +235,16 @@
|
||||
"marker.pulse_a11y_presence": "Обнаружено присутствие",
|
||||
"marker.pulse_a11y_transition": "Изменение состояния",
|
||||
"marker.pulse_a11y_running": "Работает",
|
||||
"marker.state_a11y_neutral": "Обычное состояние",
|
||||
"marker.state_a11y_working": "Работает",
|
||||
"marker.state_a11y_open": "Открыто",
|
||||
"marker.state_a11y_locked": "Заблокировано",
|
||||
"marker.state_a11y_unlocked": "Разблокировано",
|
||||
"marker.state_a11y_alarm": "Тревога",
|
||||
"marker.state_a11y_unavailable": "Недоступно",
|
||||
"marker.lqi_a11y_low": "LQI {value}, слабый сигнал",
|
||||
"marker.lqi_a11y_mid": "LQI {value}, средний сигнал",
|
||||
"marker.lqi_a11y_high": "LQI {value}, сильный сигнал",
|
||||
"marker.size_label": "Размер / поворот значка",
|
||||
"marker.angle_label": "Поворот",
|
||||
"marker.model_label": "Модель",
|
||||
|
||||
+14
-4
@@ -33,10 +33,11 @@ import {
|
||||
} from './render/opening-symbol';
|
||||
import { activeRegistryHass, fullRegistryHass, type HaRegistrySnapshot } from './ha-binding-status';
|
||||
import {
|
||||
resolveDevicePresentation, type PresentationActivityRuntime, type ResolvedDevicePresentation,
|
||||
deviceA11yState, resolveDevicePresentation,
|
||||
type PresentationActivityRuntime, type ResolvedDevicePresentation,
|
||||
} from './device-presentation';
|
||||
import { presentationSnapshotKey } from './render-device-snapshot';
|
||||
import { deviceFaceStyle, renderDeviceFace } from './device-face';
|
||||
import { deviceFaceStyle, deviceThemeClass, renderDeviceFace } from './device-face';
|
||||
import { valueBadgeTitle } from './device-value-badge';
|
||||
import { contentFingerprint } from './visual-continuity';
|
||||
import type { VirtualLightSnapshot } from './virtual-light-state';
|
||||
@@ -361,15 +362,24 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
reducedMotion: o.reducedMotion,
|
||||
});
|
||||
const st = [`left:${left}%`, `top:${top}%`, ...deviceFaceStyle(presentation)];
|
||||
const a11yState = deviceA11yState(presentation);
|
||||
const deviceAriaLabel = [
|
||||
d.name,
|
||||
t(o.lang, (`marker.state_a11y_${a11yState}`) as any),
|
||||
presentation.pulse.kind !== 'none'
|
||||
? t(o.lang, (`marker.pulse_a11y_${presentation.pulse.reason}`) as any) : '',
|
||||
presentation.valueFullText || presentation.valueText || '',
|
||||
valueBadgeTitle(presentation.valueBadge),
|
||||
presentation.lqiText != null && presentation.lqiBand
|
||||
? t(o.lang, (`marker.lqi_a11y_${presentation.lqiBand}`) as any, {
|
||||
value: presentation.lqiText,
|
||||
}) : '',
|
||||
].filter(Boolean).join(', ');
|
||||
return html`<div class="dev ${presentation.classes.join(' ')} ${d.virtual ? 'virtual' : ''} ${presentation.valueText != null ? 'valonly' : ''}"
|
||||
return html`<div class="dev ${deviceThemeClass(planHass)} ${presentation.classes.join(' ')} ${d.virtual ? 'virtual' : ''} ${presentation.valueText != null ? 'valonly' : ''}"
|
||||
data-hp="device" data-id="${d.id}" data-entity=${d.primary || nothing} data-area=${d.area || nothing}
|
||||
aria-label=${deviceAriaLabel}
|
||||
role="img" aria-label=${deviceAriaLabel}
|
||||
data-state=${a11yState}
|
||||
data-lqi-band=${presentation.lqiText != null ? presentation.lqiBand || nothing : nothing}
|
||||
data-binding-status=${d.bindingStatus?.kind === 'ha_disabled' ? 'ha-disabled' : d.bindingStatus?.kind || 'active'}
|
||||
data-disabled-reason=${presentation.disabledReason ? presentation.disabledReason.replace('_', '-') : nothing}
|
||||
style="${st.join(';')}">
|
||||
|
||||
+219
-106
@@ -806,8 +806,8 @@ export const cardStyles = css`
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: calc(var(--dev-size) * var(--ripple-scale, 3));
|
||||
height: calc(var(--dev-size) * var(--ripple-scale, 3));
|
||||
width: var(--dev-size);
|
||||
height: var(--dev-size);
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
@@ -821,39 +821,41 @@ export const cardStyles = css`
|
||||
}
|
||||
/* A witnessed edge: exactly three waves over the 3.3 s runtime window. */
|
||||
.device-pulse.short i {
|
||||
animation: hp-pulse-short 1.1s ease-out 1 forwards;
|
||||
animation: hp-pulse-short 1.1s cubic-bezier(.22,.61,.36,1) 1 forwards;
|
||||
}
|
||||
.device-pulse.short i:nth-child(2) { animation-delay: 1.1s; }
|
||||
.device-pulse.short i:nth-child(3) { animation-delay: 2.2s; }
|
||||
.device-pulse.short.gen2 i { animation-name: hp-pulse-short-b; }
|
||||
.device-pulse.continuous i:nth-child(n + 2),
|
||||
.device-pulse.alarm i:nth-child(n + 2) { display: none; }
|
||||
.device-pulse.alarm i:nth-child(n + 3) { display: none; }
|
||||
.device-pulse.continuous i:first-child {
|
||||
animation: hp-pulse-continuous 2.4s ease-in-out infinite;
|
||||
animation: hp-pulse-continuous 3.6s cubic-bezier(.45,.05,.55,.95) infinite;
|
||||
}
|
||||
.device-pulse.alarm i:first-child {
|
||||
border-color: #f25a4a;
|
||||
animation: hp-pulse-alarm 1s ease-out infinite;
|
||||
.device-pulse.alarm i {
|
||||
border-width: 3px;
|
||||
border-color: #F0410C;
|
||||
animation: hp-pulse-alarm 2.4s cubic-bezier(.22,.61,.36,1) infinite;
|
||||
}
|
||||
.device-pulse.alarm i:nth-child(2) { animation-delay: 1.2s; }
|
||||
@keyframes hp-pulse-short {
|
||||
0% { transform: scale(0.18); opacity: 0.7; }
|
||||
0% { transform: scale(1); opacity: 0.55; }
|
||||
70% { opacity: 0.22; }
|
||||
100% { transform: scale(1); opacity: 0; }
|
||||
100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; }
|
||||
}
|
||||
/* Alternate identity: a rapid retrigger restarts the browser timeline. */
|
||||
@keyframes hp-pulse-short-b {
|
||||
0% { transform: scale(0.18); opacity: 0.7; }
|
||||
0% { transform: scale(1); opacity: 0.55; }
|
||||
70% { opacity: 0.22; }
|
||||
100% { transform: scale(1); opacity: 0; }
|
||||
100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; }
|
||||
}
|
||||
@keyframes hp-pulse-continuous {
|
||||
0% { transform: scale(0.26); opacity: 0.5; }
|
||||
0% { transform: scale(1); opacity: 0.55; }
|
||||
65% { opacity: 0.18; }
|
||||
100% { transform: scale(1); opacity: 0; }
|
||||
100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; }
|
||||
}
|
||||
@keyframes hp-pulse-alarm {
|
||||
0% { transform: scale(0.3); opacity: 0.95; }
|
||||
100% { transform: scale(1); opacity: 0; }
|
||||
0% { transform: scale(1); opacity: 0.72; }
|
||||
100% { transform: scale(1.5); opacity: 0; }
|
||||
}
|
||||
.activity-dot {
|
||||
position: absolute;
|
||||
@@ -1789,23 +1791,19 @@ export const cardStyles = css`
|
||||
width: 130px;
|
||||
}
|
||||
.dev.valonly {
|
||||
/* all satellite metrics from --dev-size, not --icon-size: the per-device
|
||||
size multiplier must scale the value plate with its marker — same bug
|
||||
class as the v1.51.3 glyph fix (pinned to base size, the multiplier
|
||||
grew nothing). */
|
||||
width: auto;
|
||||
min-width: var(--dev-size, var(--icon-size, 2.5cqw));
|
||||
max-width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 4);
|
||||
box-sizing: border-box;
|
||||
padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.16);
|
||||
/* Saved coordinates remain the centre of a Text shell. The shell may
|
||||
expand, while the invisible anchor stays one core diameter wide. */
|
||||
width: var(--dev-size, var(--icon-size, 2.5cqw));
|
||||
}
|
||||
.dev.valonly .valtext {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.45);
|
||||
font-weight: 700;
|
||||
.dev.valonly .device-core {
|
||||
width: max-content;
|
||||
min-width: var(--dev-size, var(--icon-size, 2.5cqw));
|
||||
padding-inline: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.16);
|
||||
}
|
||||
.dev .valtext {
|
||||
overflow: visible;
|
||||
font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45));
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* RGB lights: the bulb takes the light's actual color */
|
||||
@@ -1836,14 +1834,6 @@ export const cardStyles = css`
|
||||
.sunlayer, .sunlayer.out { animation: none; }
|
||||
.sunlayer.out { opacity: 0; }
|
||||
}
|
||||
/* alarms pulse red over everything */
|
||||
.dev.alarm,
|
||||
.dev.alarm:hover {
|
||||
background: #6f2325;
|
||||
border-color: #f25a4a;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 10px rgba(242, 90, 74, 0.65);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.device-pulse { display: none; }
|
||||
}
|
||||
@@ -1869,22 +1859,122 @@ export const cardStyles = css`
|
||||
position: absolute;
|
||||
/* per-device multiplier on top of the card-wide icon size */
|
||||
--dev-size: calc(var(--icon-size, 2.5cqw) * var(--dev-scale, 1));
|
||||
/* центр квадрата (включая рамку 1px) точно на точке привязки: -(size/2 + border) */
|
||||
/* 101.5/80 shell/core ratio includes the 1 px stroke on both sides. */
|
||||
--device-shell-pad: max(0px, calc(var(--dev-size) * 0.134375 - 1px));
|
||||
--device-shell-size: calc(var(--dev-size) * 1.26875);
|
||||
--device-core-bg: var(--card-background-color, #fff);
|
||||
--device-core-fg: var(--primary-text-color, #252525);
|
||||
--device-core-bg: light-dark(#fff, #252525);
|
||||
--device-core-fg: light-dark(#252525, #fff);
|
||||
/* The saved point is the exact centre of the icon core. */
|
||||
width: var(--dev-size);
|
||||
height: var(--dev-size);
|
||||
margin: calc(var(--dev-size) / -2) 0 0 calc(var(--dev-size) / -2);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
display: block;
|
||||
color: var(--device-core-fg);
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.2s;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
z-index: 2;
|
||||
}
|
||||
.dev.theme-light {
|
||||
--device-core-bg: #fff;
|
||||
--device-core-fg: #252525;
|
||||
}
|
||||
.dev.theme-dark {
|
||||
--device-core-bg: #252525;
|
||||
--device-core-fg: #fff;
|
||||
}
|
||||
.dev::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: max(44px, var(--device-shell-size));
|
||||
height: max(44px, var(--device-shell-size));
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.device-shell {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(var(--dev-size) * 0.1);
|
||||
padding: var(--device-shell-pad);
|
||||
min-width: var(--device-shell-size);
|
||||
min-height: var(--device-shell-size);
|
||||
border: 1px solid #BCBCBC;
|
||||
border-radius: calc(var(--device-shell-size) / 2);
|
||||
background: transparent;
|
||||
box-shadow:
|
||||
0 1px 2px rgb(37 40 45 / 12%),
|
||||
0 4px 8px -1.07px rgb(37 40 45 / 18%);
|
||||
transition: border-color .15s, box-shadow .15s, opacity .2s;
|
||||
pointer-events: none;
|
||||
/* Normative production fallback: never add a per-marker backdrop blur. */
|
||||
backdrop-filter: none;
|
||||
}
|
||||
.device-shell:not(.with-values) {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.device-shell.with-values.pos-right {
|
||||
left: calc(50% - var(--device-shell-size) / 2);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.device-shell.with-values.pos-left {
|
||||
right: calc(50% - var(--device-shell-size) / 2);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.device-shell.with-values.pos-bottom {
|
||||
left: 50%;
|
||||
top: calc(50% - var(--device-shell-size) / 2);
|
||||
transform: translateX(-50%);
|
||||
flex-direction: column;
|
||||
}
|
||||
.device-shell.with-values.pos-top {
|
||||
left: 50%;
|
||||
bottom: calc(50% - var(--device-shell-size) / 2);
|
||||
transform: translateX(-50%);
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.device-core {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 auto;
|
||||
width: var(--dev-size);
|
||||
height: var(--dev-size);
|
||||
margin: calc(var(--dev-size) / -2 - 1px) 0 0 calc(var(--dev-size) / -2 - 1px);
|
||||
border-radius: 22%;
|
||||
background: var(--hp-bg);
|
||||
border: 1px solid var(--hp-line);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--hp-txt);
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: background 0.15s, border-color 0.15s, opacity 0.2s;
|
||||
box-shadow: var(--shadow-1);
|
||||
z-index: 2;
|
||||
border-radius: 28%;
|
||||
background: var(--device-core-bg);
|
||||
color: var(--device-core-fg);
|
||||
line-height: 0;
|
||||
transition: background .15s, color .15s, opacity .2s;
|
||||
}
|
||||
.device-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
gap: calc(var(--dev-size) * .08);
|
||||
min-width: 0;
|
||||
}
|
||||
.device-shell.pos-top .device-sections,
|
||||
.device-shell.pos-bottom .device-sections {
|
||||
flex-direction: row;
|
||||
}
|
||||
.dev ha-icon {
|
||||
/* from --dev-size, NOT --icon-size: the per-device size multiplier must
|
||||
@@ -1899,26 +1989,39 @@ export const cardStyles = css`
|
||||
}
|
||||
.stage.mode-devices .dev { cursor: grab; }
|
||||
.stage.mode-devices .dev:active { cursor: grabbing; }
|
||||
.dev.on {
|
||||
background: var(--hp-on);
|
||||
border-color: var(--hp-on);
|
||||
.dev.on .device-core {
|
||||
background: #F0A00C;
|
||||
color: #503c00;
|
||||
}
|
||||
.dev.open {
|
||||
.dev.open .device-core {
|
||||
background: var(--hp-open);
|
||||
border-color: var(--hp-open);
|
||||
color: #4a2800;
|
||||
}
|
||||
.dev.lock-locked .device-core {
|
||||
background: var(--device-core-bg);
|
||||
color: #000;
|
||||
}
|
||||
.dev.lock-unlocked .device-core {
|
||||
background: #F0A00C;
|
||||
color: #4a2800;
|
||||
}
|
||||
/* Interaction wins ordinary state colours. Alarm keeps priority through
|
||||
the more-specific .dev.alarm:hover rule above. */
|
||||
.dev:hover {
|
||||
background: var(--hp-accent);
|
||||
the more-specific rule below. Unavailable has no visual hover. */
|
||||
.dev:not(.unavail):hover .device-core {
|
||||
background: #0C82F0;
|
||||
color: var(--text-primary-color, #fff);
|
||||
z-index: 5;
|
||||
}
|
||||
.dev:hover,
|
||||
.dev:focus-visible { z-index: 5; }
|
||||
.dev:not(.unavail):hover .device-shell {
|
||||
border-color: #0C82F0;
|
||||
}
|
||||
.dev.unavail {
|
||||
opacity: 0.35;
|
||||
}
|
||||
.dev.unavail .device-core {
|
||||
background: #B5BAC1;
|
||||
}
|
||||
.physical-hit {
|
||||
fill: transparent;
|
||||
stroke: transparent;
|
||||
@@ -1971,7 +2074,7 @@ export const cardStyles = css`
|
||||
.drawwall.invalid .rangehint { color: var(--error-color, #db4437); opacity: 1; }
|
||||
.stage.markup.tool-partition,
|
||||
.stage.markup.tool-column { cursor: crosshair; }
|
||||
.dev.virtual {
|
||||
.dev.virtual .device-shell {
|
||||
border-style: dashed;
|
||||
}
|
||||
/* "hide from plan" flag, shown only in the device editor with the
|
||||
@@ -1981,18 +2084,28 @@ export const cardStyles = css`
|
||||
(owner's request). */
|
||||
.dev.ghost {
|
||||
opacity: 0.6;
|
||||
color: var(--hp-accent);
|
||||
}
|
||||
.dev.ghost .device-shell {
|
||||
border-style: dashed;
|
||||
border-color: var(--hp-accent);
|
||||
box-shadow: none;
|
||||
}
|
||||
.dev.ghost .device-core {
|
||||
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;
|
||||
}
|
||||
/* HA-disabled is not a user hide: neutral grey + a power-off badge keeps
|
||||
the two ghosts distinguishable without relying on colour alone. */
|
||||
.dev.ghost.ha-disabled {
|
||||
opacity: 0.62;
|
||||
color: var(--secondary-text-color, #9aa0aa);
|
||||
}
|
||||
.dev.ghost.ha-disabled .device-shell {
|
||||
border-color: var(--secondary-text-color, #9aa0aa);
|
||||
}
|
||||
.dev.ghost.ha-disabled .device-core {
|
||||
background: rgba(120, 124, 134, 0.2);
|
||||
background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 24%, var(--card-background-color, #1c2530));
|
||||
color: var(--secondary-text-color, #9aa0aa);
|
||||
@@ -2030,74 +2143,74 @@ export const cardStyles = css`
|
||||
background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 10%, transparent);
|
||||
}
|
||||
.habindingbanner.limited > ha-icon { color: var(--secondary-text-color, #9aa0aa); }
|
||||
.dev.sel {
|
||||
border-color: #ffc14d;
|
||||
box-shadow: 0 0 0 3px rgba(255, 193, 77, 0.35);
|
||||
.dev.sel .device-shell {
|
||||
border-color: #0C82F0;
|
||||
box-shadow:
|
||||
0 0 0 2px var(--card-background-color, #fff),
|
||||
0 0 0 4px rgb(12 130 240 / 65%),
|
||||
0 4px 8px -1.07px rgb(37 40 45 / 18%);
|
||||
}
|
||||
.dev:focus-visible .device-shell {
|
||||
border-color: #0C82F0;
|
||||
box-shadow:
|
||||
0 0 0 3px rgb(12 130 240 / 42%),
|
||||
0 0 0 6px rgb(12 130 240 / 18%),
|
||||
0 4px 8px -1.07px rgb(37 40 45 / 18%);
|
||||
}
|
||||
/* Alert stays above focus, selection, hover and ordinary semantic paint. */
|
||||
.dev.alarm .device-core,
|
||||
.dev.alarm:hover .device-core,
|
||||
.dev.alarm:focus-visible .device-core {
|
||||
background: #F0410C;
|
||||
color: #fff;
|
||||
}
|
||||
.dev.alarm .device-shell,
|
||||
.dev.alarm:hover .device-shell,
|
||||
.dev.alarm:focus-visible .device-shell {
|
||||
border-color: #F0410C;
|
||||
box-shadow: 0 0 0 3px rgb(240 65 12 / 24%),
|
||||
0 4px 8px -1.07px rgb(37 40 45 / 18%);
|
||||
}
|
||||
|
||||
.dev .value-badge {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
max-width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 4);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: var(--card-background-color, var(--hp-bg));
|
||||
border: 1px solid var(--hp-accent, #ff9800);
|
||||
width: max-content;
|
||||
min-width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875);
|
||||
height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: visible;
|
||||
background: var(--device-core-bg);
|
||||
border: 0;
|
||||
border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.18);
|
||||
padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.14);
|
||||
font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.45);
|
||||
font-weight: 700;
|
||||
line-height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.68);
|
||||
color: var(--hp-txt);
|
||||
font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45));
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: var(--device-core-fg);
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dev .value-badge.tone-humidity { border-color: #4fc3f7; }
|
||||
.dev .value-badge.unavailable,
|
||||
.dev .value-badge.missing { opacity: 0.66; }
|
||||
.dev .value-badge.pos-right {
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-left: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.1);
|
||||
}
|
||||
.dev .value-badge.legacy-secondary.pos-right {
|
||||
top: calc(50% + var(--dev-size, var(--icon-size, 2.5cqw)) * 0.78);
|
||||
}
|
||||
.dev .value-badge.pos-left {
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-right: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.1);
|
||||
}
|
||||
.dev .value-badge.pos-top {
|
||||
left: 50%;
|
||||
bottom: 100%;
|
||||
transform: translateX(-50%);
|
||||
margin-bottom: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.1);
|
||||
}
|
||||
.dev .value-badge.pos-bottom {
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.1);
|
||||
}
|
||||
.dev .lqi {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
top: calc(50% + var(--device-shell-size) / 2 + var(--dev-size) * .05);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.05);
|
||||
font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.38);
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
text-shadow: 0 0 3px rgba(0, 0, 0, 0.9), 0 0 2px rgba(0, 0, 0, 0.9);
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dev .lqi.below-value-badge {
|
||||
margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.88);
|
||||
margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.8875);
|
||||
}
|
||||
.editbar {
|
||||
display: grid;
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ export interface Marker {
|
||||
/** `ripple` is legacy read compatibility; UI maps it to icon_ripple. */
|
||||
display?: 'badge' | 'ripple' | 'icon_ripple' | 'value' | 'static_icon' | null; // how the device is drawn
|
||||
ripple_color?: string | null;
|
||||
ripple_size?: number | null; // max ring diameter, in icon diameters (default 3)
|
||||
ripple_size?: number | null; // max ring diameter, in icon diameters (default 1.5)
|
||||
size?: number | null; // icon size multiplier (default 1)
|
||||
angle?: number | null; // icon rotation, degrees
|
||||
/** Entities this icon toggles as a group (wall switch → its lights). */
|
||||
|
||||
Reference in New Issue
Block a user