mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
feat: background color around the plan — global + per-space (HP-1554)
New setting 'background around the plan' (#rrggbb): - global: config.settings.bg_color, edited in the gear dialog with a live preview and a 'theme default' reset (empty = keep the stylesheet default); - per-space override in the space dialog next to the room colors, empty = inherit the general setting (the show_lqi/fill_mode pattern); - applied to the stage in view and kiosk modes (editors keep their own canvas) and to the static houseplan-space-card; - backend validates both keys with the same strict #rrggbb match as room_color; garbage strings are rejected (test_bg_color_setting). smoke_bg_color covers apply/override/inherit/reset, dialog previews, the wire format of the cleared override, kiosk and the static card.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -173,6 +173,8 @@ SPACE_DISPLAY_SCHEMA = vol.Schema(
|
||||
vol.Optional("show_borders"): bool,
|
||||
vol.Optional("show_names"): bool,
|
||||
vol.Optional("room_color"): vol.Match(r"^#[0-9a-fA-F]{6}$"),
|
||||
# per-space background around the plan; absent = inherit the global one
|
||||
vol.Optional("bg_color"): vol.Match(r"^#[0-9a-fA-F]{6}$"),
|
||||
vol.Optional("room_opacity"): vol.All(vol.Coerce(float), vol.Range(min=0, max=1)),
|
||||
vol.Optional("fill_mode"): vol.In(["none", "lqi", "light", "temp", "glow"]),
|
||||
vol.Optional("temp_min"): vol.Coerce(float),
|
||||
@@ -319,6 +321,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional("settings", default=dict): vol.Schema(
|
||||
{
|
||||
vol.Optional("glow_radius_cm"): vol.All(vol.Coerce(float), vol.Range(min=10, max=10000)),
|
||||
# background around the plan, all spaces (a space may override)
|
||||
vol.Optional("bg_color"): vol.Match(r"^#[0-9a-fA-F]{6}$"),
|
||||
vol.Optional("known_devices"): vol.All([_TEXT], vol.Length(max=MAX_KNOWN_DEVICES)),
|
||||
vol.Optional("new_device_ids"): vol.All([_TEXT], vol.Length(max=MAX_KNOWN_DEVICES)),
|
||||
vol.Optional("fill_colors"): vol.Schema(
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// New setting "background color around the plan": global (config.settings.bg_color,
|
||||
// gear dialog) with a per-space override (space dialog, empty = inherit), applied
|
||||
// to the stage in view and kiosk modes and to the static space-card. Editors keep
|
||||
// their own canvas; the open dialogs preview the pending color live.
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
const { page, browser } = await launch({ width: 900, height: 900 }, 1);
|
||||
const res = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const stageBg = async () => {
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
return getComputedStyle(sr().querySelector('.stage')).backgroundColor;
|
||||
};
|
||||
const rgb = (hex) => `rgb(${parseInt(hex.slice(1, 3), 16)}, ${parseInt(hex.slice(3, 5), 16)}, ${parseInt(hex.slice(5, 7), 16)})`;
|
||||
|
||||
const themeBg = await stageBg(); // theme default from the stylesheet
|
||||
const sp = c._serverCfg.spaces.find((s) => s.id === c._space);
|
||||
|
||||
// 1) global setting colors the stage
|
||||
c._serverCfg.settings = { ...(c._serverCfg.settings || {}), bg_color: '#224466' };
|
||||
out.globalApplies = (await stageBg()) === rgb('#224466');
|
||||
|
||||
// 2) per-space override wins
|
||||
sp.settings = { ...(sp.settings || {}), bg_color: '#663311' };
|
||||
out.spaceOverrides = (await stageBg()) === rgb('#663311');
|
||||
|
||||
// 3) clearing the space setting inherits the global again
|
||||
delete sp.settings.bg_color;
|
||||
out.clearInheritsGlobal = (await stageBg()) === rgb('#224466');
|
||||
|
||||
// 4) clearing the global returns the theme default
|
||||
delete c._serverCfg.settings.bg_color;
|
||||
out.clearRestoresTheme = (await stageBg()) === themeBg;
|
||||
|
||||
// 5) invalid stored value is ignored (renderer never trusts garbage)
|
||||
c._serverCfg.settings.bg_color = 'url(javascript:alert(1))';
|
||||
out.garbageIgnored = (await stageBg()) === themeBg;
|
||||
delete c._serverCfg.settings.bg_color;
|
||||
|
||||
// 6) the settings dialog previews its pending color live + saves it
|
||||
let saved = null;
|
||||
const origWS = c.hass.callWS;
|
||||
c.hass.callWS = async (m) => {
|
||||
if (m.type === 'houseplan/config/set') { saved = m.config; return { rev: (c._cfgRev || 0) + 1 }; }
|
||||
return origWS.call(c.hass, m);
|
||||
};
|
||||
c._openSettingsDialog();
|
||||
await c.updateComplete;
|
||||
out.dialogHasBgRow = !!sr().querySelector('.dialog .colorrow input[type=color]');
|
||||
c._settingsDialog = { ...c._settingsDialog, bgColor: '#0a2a4a' };
|
||||
out.dialogPreviews = (await stageBg()) === rgb('#0a2a4a');
|
||||
await c._saveSettingsDialog();
|
||||
out.dialogSaves = saved?.settings?.bg_color === '#0a2a4a';
|
||||
out.savedApplies = (await stageBg()) === rgb('#0a2a4a');
|
||||
|
||||
// 7) the space dialog previews and saves the override
|
||||
c._openSpaceDialog('edit', c._space);
|
||||
await c.updateComplete;
|
||||
c._spaceDialog = { ...c._spaceDialog, bgColor: '#4a0a2a' };
|
||||
out.spaceDialogPreviews = (await stageBg()) === rgb('#4a0a2a');
|
||||
saved = null;
|
||||
await c._saveSpaceDialog();
|
||||
const savedSp = saved?.spaces?.find((s) => s.id === c._space);
|
||||
out.spaceDialogSaves = savedSp?.settings?.bg_color === '#4a0a2a';
|
||||
out.spaceSavedApplies = (await stageBg()) === rgb('#4a0a2a');
|
||||
|
||||
// ...and clearing it in the dialog inherits the global again
|
||||
c._openSpaceDialog('edit', c._space);
|
||||
await c.updateComplete;
|
||||
out.spaceDialogPrefilled = c._spaceDialog.bgColor === '#4a0a2a';
|
||||
c._spaceDialog = { ...c._spaceDialog, bgColor: null };
|
||||
await c._saveSpaceDialog();
|
||||
// over the wire `undefined` keys vanish — check the JSON form, like the backend sees it
|
||||
const wire = JSON.parse(JSON.stringify(saved));
|
||||
const savedSp2 = wire?.spaces?.find((s) => s.id === c._space);
|
||||
out.spaceDialogClears = !('bg_color' in (savedSp2?.settings || {}));
|
||||
out.backToGlobal = (await stageBg()) === rgb('#0a2a4a');
|
||||
c.hass.callWS = origWS;
|
||||
|
||||
// 8) editors keep their own canvas (inline bg only in view/kiosk)
|
||||
c._setMode('decor');
|
||||
const editorBg = await stageBg();
|
||||
out.editorUnpainted = editorBg !== rgb('#0a2a4a');
|
||||
c._setMode('view');
|
||||
out.viewPaintedAgain = (await stageBg()) === rgb('#0a2a4a');
|
||||
|
||||
// 9) kiosk instance respects the setting
|
||||
const k = document.createElement('houseplan-card');
|
||||
k.setConfig({ type: 'custom:houseplan-card', kiosk: true, cycle: 0 });
|
||||
k.hass = c.hass;
|
||||
k.style.cssText = 'position:fixed;left:0;top:0;width:860px;height:860px;z-index:99';
|
||||
document.body.appendChild(k);
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
// the kiosk instance re-reads the pristine demo config — inject the color
|
||||
// into ITS copy; what matters here is the kiosk rendering path
|
||||
k._serverCfg.settings = { ...(k._serverCfg.settings || {}), bg_color: '#0a2a4a' };
|
||||
k.hass = { ...c.hass };
|
||||
k.requestUpdate();
|
||||
await k.updateComplete;
|
||||
const kStage = (k.shadowRoot || k.renderRoot).querySelector('.stage');
|
||||
out.kioskApplies = !!kStage && getComputedStyle(kStage).backgroundColor === rgb('#0a2a4a');
|
||||
k.remove();
|
||||
|
||||
// 10) the static space-card paints the same background
|
||||
await customElements.whenDefined('houseplan-space-card');
|
||||
const cfg = JSON.parse(JSON.stringify(c._serverCfg));
|
||||
cfg.settings = { ...(cfg.settings || {}), bg_color: '#123456' };
|
||||
const hass2 = { ...c.hass, callWS: async (m) => {
|
||||
if (m.type === 'houseplan/config/get') return { config: cfg, rev: 1 };
|
||||
if (m.type === 'houseplan/layout/get') return { layout: {} };
|
||||
return origWS.call(c.hass, m);
|
||||
} };
|
||||
const scHost = document.createElement('div');
|
||||
document.body.appendChild(scHost);
|
||||
const sc = document.createElement('houseplan-space-card');
|
||||
sc.setConfig({ type: 'custom:houseplan-space-card', space: c._space });
|
||||
sc.hass = hass2;
|
||||
scHost.appendChild(sc);
|
||||
const t0 = Date.now();
|
||||
while (!sc.renderRoot?.querySelector('.hp-static-stage') && Date.now() - t0 < 6000) await new Promise((r) => setTimeout(r, 80));
|
||||
await sc.updateComplete;
|
||||
const st = sc.renderRoot.querySelector('.hp-static-stage');
|
||||
out.staticCardApplies = !!st && getComputedStyle(st).backgroundColor === rgb('#123456');
|
||||
// ...and the space override beats the global there too
|
||||
cfg.spaces.find((s) => s.id === c._space).settings = { bg_color: '#654321' };
|
||||
sc.hass = { ...hass2 };
|
||||
sc._cfg = null; // force a config re-read
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
sc.requestUpdate(); await sc.updateComplete;
|
||||
const st2 = sc.renderRoot.querySelector('.hp-static-stage');
|
||||
out.staticCardSpaceWins = !!st2 && getComputedStyle(st2).backgroundColor === rgb('#654321');
|
||||
scHost.remove();
|
||||
|
||||
// tidy: drop the color so later smokes on this page see the theme default
|
||||
delete c._serverCfg.settings.bg_color;
|
||||
const spx = c._serverCfg.spaces.find((s) => s.id === c._space);
|
||||
if (spx?.settings) delete spx.settings.bg_color;
|
||||
await stageBg();
|
||||
return out;
|
||||
});
|
||||
checkAll(res);
|
||||
await finish(browser, res);
|
||||
@@ -28,8 +28,8 @@ const res = await page.evaluate(async () => {
|
||||
});
|
||||
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||
checkAll(res, {
|
||||
"rows": 11,
|
||||
"groups": ["Fill: lights", "Fill: temperature", "Fill: zigbee signal", "Light-sources fill"],
|
||||
"rows": 12, // 10 цветов + радиус свечения + фон вокруг плана
|
||||
"groups": ["Fill: lights", "Fill: temperature", "Fill: zigbee signal", "Light-sources fill", "Stage background"],
|
||||
"saved": {"c": "#ff00ff", "a": 0.5},
|
||||
"lqiBefore": 7,
|
||||
"lqiAfter": 0,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+53
-39
File diff suppressed because one or more lines are too long
+65
-5
@@ -19,7 +19,7 @@ import {
|
||||
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
||||
stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, glowColorOf, doorSector, hasRoomBehind, controlsAction, isControllable,
|
||||
spaceDisplayOf, roomFillStyle, fillColorsOf, DEFAULT_FILL_COLORS, type FillColors, runServiceFor, RUN_TARGET_DOMAINS,
|
||||
isActiveState, DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY,
|
||||
isActiveState, DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY, stageBgOf,
|
||||
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
|
||||
referencedContentUrls,
|
||||
DISPLAY_MODES, TAP_ACTIONS, SPACE_FILL_MODES, ROOM_FILL_MODES,
|
||||
@@ -294,7 +294,7 @@ class HouseplanCard extends LitElement {
|
||||
private _onboardingShown = false; // the auto space dialog is shown once per session
|
||||
|
||||
private _rulesDialog: { rules: IconRule[]; test: string; busy: boolean } | null = null;
|
||||
private _settingsDialog: { colors: FillColors; glowRadius: number; busy: boolean } | null = null;
|
||||
private _settingsDialog: { colors: FillColors; glowRadius: number; bgColor: string | null; busy: boolean } | null = null;
|
||||
private _importDialog: { floors: (FloorInfo & { checked: boolean })[] } | null = null;
|
||||
private _importQueue: string[] = []; // floor titles still to create
|
||||
private _importTotal = 0;
|
||||
@@ -363,6 +363,7 @@ class HouseplanCard extends LitElement {
|
||||
showNames: boolean;
|
||||
roomColor: string;
|
||||
roomOpacity: number; // 0..1
|
||||
bgColor: string | null; // background around the plan; null = inherit general
|
||||
fillMode: 'none' | 'lqi' | 'light' | 'temp' | 'glow';
|
||||
tempMin: number;
|
||||
tempMax: number;
|
||||
@@ -3776,6 +3777,7 @@ class HouseplanCard extends LitElement {
|
||||
source: sp.plan_url ? 'file' : 'draw',
|
||||
showBorders: disp.showBorders, showNames: disp.showNames,
|
||||
roomColor: disp.color, roomOpacity: disp.opacity, fillMode: disp.fill,
|
||||
bgColor: disp.bgColor,
|
||||
tempMin: disp.tempMin, tempMax: disp.tempMax,
|
||||
showLqi: disp.showLqi ?? this._config?.show_signal ?? true,
|
||||
cardFontScale: disp.cardFontScale,
|
||||
@@ -3790,6 +3792,7 @@ class HouseplanCard extends LitElement {
|
||||
source: 'file',
|
||||
showBorders: false, showNames: false,
|
||||
roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'glow',
|
||||
bgColor: null,
|
||||
tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX,
|
||||
showLqi: this._config?.show_signal ?? true,
|
||||
cardFontScale: 1,
|
||||
@@ -4017,6 +4020,7 @@ class HouseplanCard extends LitElement {
|
||||
show_names: draw && d.mode === 'create' ? true : d.showNames,
|
||||
room_color: d.roomColor,
|
||||
room_opacity: d.roomOpacity,
|
||||
bg_color: d.bgColor || undefined, // empty = inherit the general setting
|
||||
fill_mode: d.fillMode,
|
||||
temp_min: Number.isFinite(d.tempMin) ? Math.min(d.tempMin, d.tempMax) : DEFAULT_TEMP_MIN,
|
||||
temp_max: Number.isFinite(d.tempMax) ? Math.max(d.tempMin, d.tempMax) : DEFAULT_TEMP_MAX,
|
||||
@@ -4123,6 +4127,7 @@ class HouseplanCard extends LitElement {
|
||||
source: 'file',
|
||||
showBorders: false, showNames: false,
|
||||
roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'glow',
|
||||
bgColor: null,
|
||||
tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX,
|
||||
showLqi: this._config?.show_signal ?? true,
|
||||
cardFontScale: 1,
|
||||
@@ -4178,6 +4183,31 @@ class HouseplanCard extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective stage background: per-space override → global setting → '' (the
|
||||
* theme default from the stylesheet). An open dialog previews its pending
|
||||
* value, so the color can be picked against the live plan.
|
||||
*/
|
||||
private _stageBg(disp: SpaceDisplay): string {
|
||||
const gd = this._settingsDialog;
|
||||
const sd = this._spaceDialog;
|
||||
const globalBg = gd ? gd.bgColor || '' : stageBgOf(this._settings, { bgColor: null });
|
||||
const spaceBg = sd && sd.mode === 'edit' && sd.spaceId === this._space
|
||||
? sd.bgColor || ''
|
||||
: disp.bgColor || '';
|
||||
return spaceBg || globalBg;
|
||||
}
|
||||
|
||||
/** Current computed stage background as #rrggbb — the color input's default. */
|
||||
private _stageBgHex(): string {
|
||||
const st = this._stageEl;
|
||||
if (st) {
|
||||
const m = getComputedStyle(st).backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||
if (m) return '#' + m.slice(1, 4).map((n) => (+n).toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
return '#111111';
|
||||
}
|
||||
|
||||
// ================= GENERAL SETTINGS =================
|
||||
|
||||
private _openSettingsDialog = (): void => {
|
||||
@@ -4187,7 +4217,12 @@ class HouseplanCard extends LitElement {
|
||||
const glowRadius = this._imperial
|
||||
? Math.round((cm / 30.48) * 10) / 10
|
||||
: Math.round(cm) / 100;
|
||||
this._settingsDialog = { colors: JSON.parse(JSON.stringify(this._fillColors)), glowRadius, busy: false };
|
||||
this._settingsDialog = {
|
||||
colors: JSON.parse(JSON.stringify(this._fillColors)),
|
||||
glowRadius,
|
||||
bgColor: stageBgOf(this._settings, { bgColor: null }) || null,
|
||||
busy: false,
|
||||
};
|
||||
};
|
||||
|
||||
private _setFillColor(key: keyof FillColors, patch: Partial<{ c: string; a: number }>): void {
|
||||
@@ -4208,6 +4243,8 @@ class HouseplanCard extends LitElement {
|
||||
const cm = this._imperial ? d.glowRadius * 30.48 : d.glowRadius * 100;
|
||||
if (Number.isFinite(cm) && cm > 0 && Math.round(cm) !== 300) settings.glow_radius_cm = Math.round(cm);
|
||||
else delete settings.glow_radius_cm;
|
||||
if (d.bgColor) settings.bg_color = d.bgColor;
|
||||
else delete settings.bg_color;
|
||||
this._serverCfg = { ...cfg, settings };
|
||||
await this._saveConfigNow();
|
||||
this._settingsDialog = null;
|
||||
@@ -4360,10 +4397,21 @@ class HouseplanCard extends LitElement {
|
||||
}} />
|
||||
<span class="opl">${this._imperial ? this._t('gs.unit_ft') : this._t('gs.unit_m')}</span>
|
||||
</div>
|
||||
<label class="dispsection">${this._t('gs.bg_group')}</label>
|
||||
<div class="colorrow gsrow">
|
||||
<span class="gsl">${this._t('gs.bg_color')}</span>
|
||||
<input type="color" .value=${this._settingsDialog!.bgColor || this._stageBgHex()}
|
||||
@input=${(e: Event) =>
|
||||
(this._settingsDialog = { ...this._settingsDialog!, bgColor: (e.target as HTMLInputElement).value })} />
|
||||
${this._settingsDialog!.bgColor
|
||||
? html`<button class="btn ghost" @click=${() =>
|
||||
(this._settingsDialog = { ...this._settingsDialog!, bgColor: null })}>${this._t('gs.bg_default')}</button>`
|
||||
: html`<span class="opl">${this._t('gs.bg_theme')}</span>`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="btn ghost" @click=${() =>
|
||||
(this._settingsDialog = { ...this._settingsDialog!, colors: JSON.parse(JSON.stringify(DEFAULT_FILL_COLORS)), glowRadius: this._imperial ? 9.8 : 3 })}>
|
||||
(this._settingsDialog = { ...this._settingsDialog!, colors: JSON.parse(JSON.stringify(DEFAULT_FILL_COLORS)), glowRadius: this._imperial ? 9.8 : 3, bgColor: null })}>
|
||||
${this._t('gs.reset')}
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
@@ -4557,6 +4605,9 @@ class HouseplanCard extends LitElement {
|
||||
const cfgSize = this._config.icon_size ?? 2.5;
|
||||
const iconPct = cfgSize > 8 ? 2.5 : cfgSize;
|
||||
const view = this._viewOr(vb);
|
||||
// Background around the plan (view/kiosk; editors keep their own canvas).
|
||||
// Both settings dialogs preview their pending value live.
|
||||
const stageBg = this._editing ? '' : this._stageBg(disp);
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
@@ -4628,7 +4679,7 @@ class HouseplanCard extends LitElement {
|
||||
</div>
|
||||
|
||||
<div class="stage ${this._markup ? 'markup tool-' + this._tool + (this._tool === 'split' && !this._splitSel ? ' pickstage' : '') + (this._tool === 'openwall' && this._openWallHover ? ' wallhot' : '') : ''} ${this._mode === 'decor' ? 'dtool-' + this._decorTool : ''} ${space.bg ? '' : 'noplan'} mode-${this._mode}"
|
||||
style="height:${this._kiosk ? '100dvh' : `calc(100dvh - ${this._hdrH}px)`}"
|
||||
style="height:${this._kiosk ? '100dvh' : `calc(100dvh - ${this._hdrH}px)`}${stageBg ? `;background:${stageBg}` : ''}"
|
||||
@click=${(e: MouseEvent) => this._markupClick(e)}
|
||||
@wheel=${(e: WheelEvent) => this._onWheel(e)}
|
||||
@pointerdown=${(e: PointerEvent) => { this._notePointer(e); this._stagePointerDown(e); }}
|
||||
@@ -6727,6 +6778,15 @@ class HouseplanCard extends LitElement {
|
||||
@input=${(e: Event) => (this._spaceDialog = { ...d, roomOpacity: Number((e.target as HTMLInputElement).value) / 100 })} />
|
||||
<span class="opv">${Math.round(d.roomOpacity * 100)}%</span>
|
||||
</div>
|
||||
<label>${this._t('space.bg_color')}</label>
|
||||
<div class="colorrow">
|
||||
<input type="color" .value=${d.bgColor || stageBgOf(this._settings, { bgColor: null }) || this._stageBgHex()}
|
||||
@input=${(e: Event) => (this._spaceDialog = { ...d, bgColor: (e.target as HTMLInputElement).value })} />
|
||||
${d.bgColor
|
||||
? html`<button class="btn ghost" @click=${() => (this._spaceDialog = { ...d, bgColor: null })}>
|
||||
${this._t('space.bg_inherit')}</button>`
|
||||
: html`<span class="opl">${this._t('space.bg_inherited')}</span>`}
|
||||
</div>
|
||||
<label>${this._t('space.fill_label')}</label>
|
||||
${SPACE_FILL_MODES.map((v) => [v, 'fill.' + v] as const).map(
|
||||
([v, k]) => html`<label class="srcrow">
|
||||
|
||||
+8
-1
@@ -379,5 +379,12 @@
|
||||
"vac.fit_mirror": "Mirror",
|
||||
"vac.trail_never": "Never",
|
||||
"vac.trail_cleaning": "While cleaning",
|
||||
"vac.trail_always": "Always"
|
||||
"vac.trail_always": "Always",
|
||||
"gs.bg_group": "Stage background",
|
||||
"gs.bg_color": "Background around the plan",
|
||||
"gs.bg_default": "Theme default",
|
||||
"gs.bg_theme": "theme default",
|
||||
"space.bg_color": "Background around the plan",
|
||||
"space.bg_inherit": "Inherit general",
|
||||
"space.bg_inherited": "inherits general settings"
|
||||
}
|
||||
|
||||
+8
-1
@@ -379,5 +379,12 @@
|
||||
"vac.fit_mirror": "Отразить",
|
||||
"vac.trail_never": "Не показывать никогда",
|
||||
"vac.trail_cleaning": "Во время уборки",
|
||||
"vac.trail_always": "Показывать всегда"
|
||||
"vac.trail_always": "Показывать всегда",
|
||||
"gs.bg_group": "Фон сцены",
|
||||
"gs.bg_color": "Цвет фона вокруг плана",
|
||||
"gs.bg_default": "Как в теме",
|
||||
"gs.bg_theme": "по умолчанию — из темы",
|
||||
"space.bg_color": "Цвет фона вокруг плана",
|
||||
"space.bg_inherit": "Наследовать общий",
|
||||
"space.bg_inherited": "наследуется из общих настроек"
|
||||
}
|
||||
|
||||
@@ -739,6 +739,8 @@ export interface SpaceDisplay {
|
||||
labelHum: boolean;
|
||||
labelLqi: boolean;
|
||||
labelLight: boolean;
|
||||
/** Background around the plan; null = inherit the global setting / theme. */
|
||||
bgColor: string | null;
|
||||
}
|
||||
|
||||
export const DEFAULT_ROOM_COLOR = '#3ea6ff';
|
||||
@@ -766,9 +768,21 @@ export function spaceDisplayOf(spaceCfg: any): SpaceDisplay {
|
||||
labelHum: s.label_hum === true,
|
||||
labelLqi: s.label_lqi === true,
|
||||
labelLight: s.label_light === true,
|
||||
bgColor: typeof s.bg_color === 'string' && /^#[0-9a-f]{6}$/i.test(s.bg_color) ? s.bg_color : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective background color around the plan: the per-space override wins,
|
||||
* then the global config.settings.bg_color, then '' — meaning "keep the
|
||||
* theme default" (the stage's stylesheet background stays untouched).
|
||||
*/
|
||||
export function stageBgOf(settings: any, disp: { bgColor: string | null }): string {
|
||||
if (disp.bgColor) return disp.bgColor;
|
||||
const g = settings?.bg_color;
|
||||
return typeof g === 'string' && /^#[0-9a-f]{6}$/i.test(g) ? g : '';
|
||||
}
|
||||
|
||||
// ---------------- global fill colors ----------------
|
||||
|
||||
export interface FillColorEntry {
|
||||
|
||||
+4
-2
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
import { html, svg, nothing, type TemplateResult } from 'lit';
|
||||
import { buildDevices, areaLqi, areaLights, areaTemp } from './devices';
|
||||
import { spaceDisplayOf, roomFillStyle, fillColorsOf, roomFillModeOf } from './logic';
|
||||
import { spaceDisplayOf, roomFillStyle, fillColorsOf, roomFillModeOf, stageBgOf } from './logic';
|
||||
import { DEFAULT_ICON_RULES, compileIconRules, EXCLUDED_DOMAINS } from './rules';
|
||||
import { t, type Lang } from './i18n';
|
||||
import type { ServerConfig } from './types';
|
||||
@@ -146,9 +146,11 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
: [];
|
||||
|
||||
const bgHref = space.bg ? (o.displayUrl ? o.displayUrl(space.bg.href) : space.bg.href) : '';
|
||||
// the static card paints the same background around the plan as the full one
|
||||
const stageBg = stageBgOf(o.cfg?.settings, disp);
|
||||
|
||||
return html`
|
||||
<div class="hp-static-stage" style="aspect-ratio:${vb[2]}/${vb[3]}">
|
||||
<div class="hp-static-stage" style="aspect-ratio:${vb[2]}/${vb[3]}${stageBg ? ';background:' + stageBg : ''}">
|
||||
<svg viewBox="${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}" preserveAspectRatio="xMidYMid meet">
|
||||
${bgHref
|
||||
? svg`<image href="${bgHref}" x="${space.bg!.x}" y="${space.bg!.y}" width="${space.bg!.w}" height="${space.bg!.h}" preserveAspectRatio="none" />`
|
||||
|
||||
@@ -164,6 +164,24 @@ def test_space_display_settings():
|
||||
v.SPACE_SCHEMA(bad_mode)
|
||||
|
||||
|
||||
def test_bg_color_setting():
|
||||
"""Background-around-the-plan color: strict #rrggbb, globally and per space."""
|
||||
ok_space = {
|
||||
"id": "f1", "title": "F", "view_box": [0, 0, 1, 1], "rooms": [],
|
||||
"settings": {"bg_color": "#102030"},
|
||||
}
|
||||
v.SPACE_SCHEMA(ok_space)
|
||||
v.CONFIG_SCHEMA({"spaces": [], "settings": {"bg_color": "#A1b2C3"}})
|
||||
with pytest.raises(vol.Invalid):
|
||||
v.SPACE_SCHEMA(dict(ok_space, settings={"bg_color": "red"}))
|
||||
with pytest.raises(vol.Invalid):
|
||||
v.CONFIG_SCHEMA({"spaces": [], "settings": {"bg_color": "url(javascript:x)"}})
|
||||
with pytest.raises(vol.Invalid):
|
||||
v.CONFIG_SCHEMA({"spaces": [], "settings": {"bg_color": "#12345"}})
|
||||
with pytest.raises(vol.Invalid):
|
||||
v.CONFIG_SCHEMA({"spaces": [], "settings": {"bg_color": "#1234567"}})
|
||||
|
||||
|
||||
def test_space_temp_bounds():
|
||||
"""Temperature comfort bounds validate as floats; temp fill mode accepted."""
|
||||
ok = {
|
||||
|
||||
Reference in New Issue
Block a user