feat v1.42.1: room-card font sizes — 3 sliders with a live preview

- space.settings.card_font_scale (tier 2 base) + room.settings
  name_scale/label_scale (tier 3), 50-300%, multiplied together and
  stacking with resize-k and kiosk per-screen multipliers
- live sample card in both the space and room dialogs
- backend schema; smoke_font_scales.mjs (9 checks); docs same-commit
This commit is contained in:
Matysh
2026-07-27 10:12:22 +03:00
parent 19e19b5c5f
commit 10d4084d17
16 changed files with 404 additions and 70 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
FILES_URL = "/houseplan_files/files"
FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.42.0"
VERSION = "1.42.1"
DEFAULT_CONFIG: dict = {
"spaces": [],
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -16,5 +16,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [],
"single_config_entry": true,
"version": "1.42.0"
"version": "1.42.1"
}
@@ -76,6 +76,8 @@ ROOM_SCHEMA = vol.All(
vol.Optional("fill_mode"): vol.Any(None, vol.In(["none", "lqi", "light", "temp"])),
vol.Optional("temp_source"): vol.Any(str, None),
vol.Optional("hum_source"): vol.Any(str, None),
vol.Optional("name_scale"): vol.Any(None, vol.All(vol.Coerce(float), vol.Range(min=0.5, max=3))),
vol.Optional("label_scale"): vol.Any(None, vol.All(vol.Coerce(float), vol.Range(min=0.5, max=3))),
},
extra=vol.ALLOW_EXTRA,
),
@@ -104,6 +106,7 @@ SPACE_DISPLAY_SCHEMA = vol.Schema(
vol.Optional("label_hum"): bool,
vol.Optional("label_lqi"): bool,
vol.Optional("label_light"): bool,
vol.Optional("card_font_scale"): vol.All(vol.Coerce(float), vol.Range(min=0.5, max=3)),
},
extra=vol.ALLOW_EXTRA,
)
+58
View File
@@ -0,0 +1,58 @@
import { launch } from './serve.mjs';
const { page, browser } = await launch();
const res = await page.evaluate(async () => {
const out = {};
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
const spId = c._space;
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== spId ? s : ({
...s, settings: { ...(s.settings || {}), show_names: true, label_temp: true } })) };
c._setMode('view'); c.requestUpdate(); await c.updateComplete;
await new Promise((r) => setTimeout(r, 100));
const lbl = () => sr().querySelector('.roomlabel');
const nameSize = () => parseFloat(getComputedStyle(lbl().querySelector('.rlname')).fontSize);
const metaEl = () => lbl().querySelector('.rlmetrics');
const n0 = nameSize();
const m0 = metaEl() ? parseFloat(getComputedStyle(metaEl()).fontSize) : null;
out.baseline = n0 > 0;
// 1) пер-комнатный name_scale ×2 — имя удвоилось, подписи нет
const room = c._curSpaceCfg.rooms.find((r) => r.name && r.area);
room.settings = { ...(room.settings || {}), name_scale: 2 };
c.requestUpdate(); await c.updateComplete;
const lblR = [...sr().querySelectorAll('.roomlabel')].find((l) => l.textContent.includes(room.name));
const n1 = parseFloat(getComputedStyle(lblR.querySelector('.rlname')).fontSize);
out.nameDoubled = Math.abs(n1 / n0 - 2) < 0.05;
const meta1 = lblR.querySelector('.rlmetrics');
out.metaUntouched = meta1 ? Math.abs(parseFloat(getComputedStyle(meta1).fontSize) - m0) < 0.5 : 'no-meta';
// 2) label_scale ×2 — подписи удвоились
room.settings.label_scale = 2;
c.requestUpdate(); await c.updateComplete;
const meta2 = lblR.querySelector('.rlmetrics');
out.metaDoubled = meta2 && m0 ? Math.abs(parseFloat(getComputedStyle(meta2).fontSize) / m0 - 2) < 0.05 : 'no-meta';
// 3) базовый множитель пространства ×1.5 — умножает всё
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== spId ? s : ({
...s, settings: { ...(s.settings || {}), card_font_scale: 1.5 } })) };
c.requestUpdate(); await c.updateComplete;
const lblR2 = [...sr().querySelectorAll('.roomlabel')].find((l) => l.textContent.includes(room.name));
const n2 = parseFloat(getComputedStyle(lblR2.querySelector('.rlname')).fontSize);
out.spaceMultiplies = Math.abs(n2 / n0 - 3) < 0.1; // 2 × 1.5
// 4) диалог комнаты: слайдеры + живой пример реагирует
c._setMode('plan'); await c.updateComplete;
const model = c._spaceModel().rooms.find((r) => r.id === room.id);
c._openRoomEdit(model); await c.updateComplete;
out.slidersPrefilled = c._roomNameScale === 2 && c._roomLabelScale === 2;
const pv = () => sr().querySelector('.cardpreview .cpname');
const p0 = parseFloat(pv().style.fontSize);
c._roomNameScale = 1; c.requestUpdate(); await c.updateComplete;
const p1 = parseFloat(pv().style.fontSize);
out.previewLive = Math.abs(p0 / p1 - 2) < 0.05;
c._roomDialogCancel(); await c.updateComplete;
// 5) превью есть и в диалоге пространства
c._openSpaceDialog('edit', spId); await c.updateComplete;
out.spacePreview = !!sr().querySelector('.cardpreview');
out.spaceSlider = [...sr().querySelectorAll('.dialog label')].some((l) => l.textContent === c._t('space.card_font'));
c._spaceDialog = null; await c.updateComplete;
return out;
});
console.log(JSON.stringify(res, null, 1));
await browser.close();
File diff suppressed because one or more lines are too long
+74 -20
View File
File diff suppressed because one or more lines are too long
+9
View File
@@ -1,5 +1,14 @@
# Changelog
## v1.42.1 — 2026-07-26 (room-card font sizes)
- Closing the "can't change the font size" feedback: **three sliders**.
Space settings gained a base room-card font size for the whole space;
Room settings gained independent sizes for the room NAME and the METRICS
line (50300% each). Effects multiply — and stack with the card's corner
resize and the kiosk per-screen multiplier as before.
- Both dialogs show a **live sample card** that follows the sliders as you
drag them.
## v1.42.0 — 2026-07-26 (room settings — the third tier)
- **Settings now have four tiers**: global → space → room → device; the more
specific tier overrides the more general one (owner's principle, fixed in
+5
View File
@@ -140,6 +140,11 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
(explicit ripple color still wins); off/white lights unchanged [auto]
- [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any
display mode; clears on 'off'; unavailable never alarms [auto]; reduced-motion static
- [ ] Card font scales (v1.42.1): three sliders — space-level base (space
dialog) plus per-room name and metrics sizes (room settings), 50300%,
multiplied together and on top of resize-k and kiosk multipliers; the
live sample card in both dialogs follows the sliders instantly; name and
metrics scale independently [auto]
- [ ] Room settings, tier 3 (v1.42.0): a gear on every room card in the Plan
editor opens Room settings (name, HA area incl. the current one, fill
override, temp/hum source); the creation dialog has the same section;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "houseplan-card",
"version": "1.42.0",
"version": "1.42.1",
"description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT",
"type": "module",
+57 -2
View File
@@ -32,7 +32,7 @@ import './space-card';
import { cardStyles } from './styles';
import { langOf, t, type I18nKey } from './i18n';
const CARD_VERSION = '1.42.0';
const CARD_VERSION = '1.42.1';
const LS_KEY = 'houseplan_card_layout_v1';
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
const LS_ZOOM = 'houseplan_card_zoom_v1';
@@ -163,6 +163,8 @@ class HouseplanCard extends LitElement {
private _roomHumSrc = '';
private _roomSrcOpen: 'temp' | 'hum' | null = null;
private _roomSrcFilter = '';
private _roomNameScale = 1;
private _roomLabelScale = 1;
// plan zoom/pan (zoom is saved per space, locally)
private _zoom = 1;
private _view: { x: number; y: number; w: number; h: number } | null = null; // current SVG viewBox (vb coordinates)
@@ -226,6 +228,7 @@ class HouseplanCard extends LitElement {
tempMin: number;
tempMax: number;
showLqi: boolean;
cardFontScale: number;
labelTemp: boolean;
labelHum: boolean;
labelLqi: boolean;
@@ -301,6 +304,8 @@ class HouseplanCard extends LitElement {
_roomHumSrc: { state: true },
_roomSrcOpen: { state: true },
_roomSrcFilter: { state: true },
_roomNameScale: { state: true },
_roomLabelScale: { state: true },
_spaceDialog: { state: true },
_infoCard: { state: true },
_rulesDialog: { state: true },
@@ -2629,6 +2634,7 @@ class HouseplanCard extends LitElement {
roomColor: disp.color, roomOpacity: disp.opacity, fillMode: disp.fill,
tempMin: disp.tempMin, tempMax: disp.tempMax,
showLqi: disp.showLqi ?? this._config?.show_signal ?? true,
cardFontScale: disp.cardFontScale,
labelTemp: disp.labelTemp, labelHum: disp.labelHum,
labelLqi: disp.labelLqi, labelLight: disp.labelLight,
cellCm: Number(sp.cell_cm) > 0 ? Number(sp.cell_cm) : 5,
@@ -2642,6 +2648,7 @@ class HouseplanCard extends LitElement {
roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'none',
tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX,
showLqi: this._config?.show_signal ?? true,
cardFontScale: 1,
labelTemp: false, labelHum: false, labelLqi: false, labelLight: false,
cellCm: 5,
busy: false,
@@ -2727,6 +2734,7 @@ class HouseplanCard extends LitElement {
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,
show_lqi: d.showLqi,
card_font_scale: d.cardFontScale !== 1 ? d.cardFontScale : undefined,
label_temp: d.labelTemp,
label_hum: d.labelHum,
label_lqi: d.labelLqi,
@@ -2823,6 +2831,7 @@ class HouseplanCard extends LitElement {
roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'none',
tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX,
showLqi: this._config?.show_signal ?? true,
cardFontScale: 1,
labelTemp: false, labelHum: false, labelLqi: false, labelLight: false,
cellCm: 5,
busy: false,
@@ -3562,6 +3571,8 @@ class HouseplanCard extends LitElement {
this._roomHumSrc = '';
this._roomSrcOpen = null;
this._roomSrcFilter = '';
this._roomNameScale = 1;
this._roomLabelScale = 1;
}
/** Open the room dialog for an EXISTING room (the gear on its card). */
@@ -3573,6 +3584,8 @@ class HouseplanCard extends LitElement {
this._roomFill = (r.settings?.fill_mode as any) || '';
this._roomTempSrc = r.settings?.temp_source || '';
this._roomHumSrc = r.settings?.hum_source || '';
this._roomNameScale = clampScale(r.settings?.name_scale);
this._roomLabelScale = clampScale(r.settings?.label_scale);
this._roomSrcOpen = null;
this._roomSrcFilter = '';
this._roomDialog = true;
@@ -3584,6 +3597,8 @@ class HouseplanCard extends LitElement {
if (this._roomFill) st.fill_mode = this._roomFill;
if (this._roomTempSrc) st.temp_source = this._roomTempSrc;
if (this._roomHumSrc) st.hum_source = this._roomHumSrc;
if (this._roomNameScale !== 1) st.name_scale = this._roomNameScale;
if (this._roomLabelScale !== 1) st.label_scale = this._roomLabelScale;
return Object.keys(st).length ? st : null;
}
@@ -3777,7 +3792,7 @@ class HouseplanCard extends LitElement {
}
}
return html`<div class="roomlabel ${rows.length ? 'card' : ''}"
style="left:${left}%;top:${top}%;color:${disp.color};opacity:${op};--rl-scale:${k}"
style="left:${left}%;top:${top}%;color:${disp.color};opacity:${op};--rl-scale:${k};--rl-space:${disp.cardFontScale};--rl-name:${clampScale(r.settings?.name_scale)};--rl-meta:${clampScale(r.settings?.label_scale)}"
@pointerdown=${(e: PointerEvent) => this._labelDown(e, r, space.id)}
@pointermove=${(e: PointerEvent) => this._labelMove(e, r, space.id)}
@pointerup=${() => this._labelUp(r)}
@@ -4652,6 +4667,13 @@ class HouseplanCard extends LitElement {
<span>${this._t(k)}</span>
</label>`,
)}
<label>${this._t('space.card_font')}</label>
<div class="colorrow gsrow">
<input type="range" min="50" max="300" step="5" .value=${String(Math.round(d.cardFontScale * 100))}
@input=${(e: Event) => (this._spaceDialog = { ...d, cardFontScale: Number((e.target as HTMLInputElement).value) / 100 })} />
<span class="opv">${Math.round(d.cardFontScale * 100)}%</span>
</div>
${this._renderCardPreview(d.cardFontScale, 1, 1)}
<label>${this._t('space.room_color')}</label>
<div class="colorrow">
<input type="color" .value=${d.roomColor}
@@ -4739,6 +4761,20 @@ class HouseplanCard extends LitElement {
</div>`;
}
/** Live sample of a room card at the given multipliers (dialog preview). */
private _renderCardPreview(spaceScale: number, nameScale: number, labelScale: number): TemplateResult {
const base = 18 * spaceScale;
return html`<div class="cardpreview">
<span class="cpname" style="font-size:${(base * nameScale).toFixed(1)}px">
${this._t('preview.room_name')}</span>
<span class="cpmeta" style="font-size:${(base * 0.62 * labelScale).toFixed(1)}px">
<ha-icon icon="mdi:thermometer"></ha-icon>22.4° ·
<ha-icon icon="mdi:water-percent"></ha-icon>45% ·
<ha-icon icon="mdi:lightbulb-on"></ha-icon>${this._t('roomcard.light_partial', { on: 1, total: 3 })}
</span>
</div>`;
}
/** One measurement-source control (average vs an explicit device/entity). */
private _renderRoomSource(kind: 'temp' | 'hum'): TemplateResult {
const val = kind === 'temp' ? this._roomTempSrc : this._roomHumSrc;
@@ -4828,6 +4864,25 @@ class HouseplanCard extends LitElement {
)}
${this._renderRoomSource('temp')}
${this._renderRoomSource('hum')}
<label class="dispsection">${this._t('room.sizes_section')}</label>
<label>${this._t('room.name_scale')}</label>
<div class="colorrow gsrow">
<input type="range" min="50" max="300" step="5" .value=${String(Math.round(this._roomNameScale * 100))}
@input=${(e: Event) => { this._roomNameScale = Number((e.target as HTMLInputElement).value) / 100; this.requestUpdate(); }} />
<span class="opv">${Math.round(this._roomNameScale * 100)}%</span>
</div>
<label>${this._t('room.label_scale')}</label>
<div class="colorrow gsrow">
<input type="range" min="50" max="300" step="5" .value=${String(Math.round(this._roomLabelScale * 100))}
@input=${(e: Event) => { this._roomLabelScale = Number((e.target as HTMLInputElement).value) / 100; this.requestUpdate(); }} />
<span class="opv">${Math.round(this._roomLabelScale * 100)}%</span>
</div>
${this._renderCardPreview(
spaceDisplayOf(this._curSpaceCfg).cardFontScale,
this._roomNameScale,
this._roomLabelScale,
)}
</div>
<div class="row">
<button class="btn ghost" @click=${this._roomDialogCancel}>${this._t('btn.cancel')}</button>
+6 -1
View File
@@ -317,5 +317,10 @@
"room.src_average": "Average over the room's sensors (default)",
"room.src_pick": "A specific HA device or entity",
"room.src_ph": "Choose a source…",
"toast.room_updated": "Room updated"
"toast.room_updated": "Room updated",
"space.card_font": "Room-card font size (whole space)",
"room.sizes_section": "Font sizes",
"room.name_scale": "Room name size",
"room.label_scale": "Metrics size",
"preview.room_name": "Living room"
}
+6 -1
View File
@@ -317,5 +317,10 @@
"room.src_average": "Средняя по датчикам комнаты (по умолчанию)",
"room.src_pick": "Конкретное устройство или сущность HA",
"room.src_ph": "Выберите источник…",
"toast.room_updated": "Комната обновлена"
"toast.room_updated": "Комната обновлена",
"space.card_font": "Размер шрифта карточек комнат (всё пространство)",
"room.sizes_section": "Размеры шрифтов",
"room.name_scale": "Размер названия",
"room.label_scale": "Размер подписей",
"preview.room_name": "Гостиная"
}
+5
View File
@@ -552,6 +552,8 @@ export interface SpaceDisplay {
tempMax: number; // comfort range upper bound, °C
/** Per-space LQI badges near zigbee devices; null = follow the card option. */
showLqi: boolean | null;
/** Base font multiplier for room cards (tier 2; rooms multiply on top). */
cardFontScale: number;
/** Room-card metrics under the room name (all default off). */
labelTemp: boolean;
labelHum: boolean;
@@ -577,6 +579,9 @@ export function spaceDisplayOf(spaceCfg: any): SpaceDisplay {
tempMin: typeof s.temp_min === 'number' ? s.temp_min : DEFAULT_TEMP_MIN,
tempMax: typeof s.temp_max === 'number' ? s.temp_max : DEFAULT_TEMP_MAX,
showLqi: typeof s.show_lqi === 'boolean' ? s.show_lqi : null,
cardFontScale: typeof s.card_font_scale === 'number' && s.card_font_scale > 0
? Math.min(3, Math.max(0.5, s.card_font_scale))
: 1,
labelTemp: s.label_temp === true,
labelHum: s.label_hum === true,
labelLqi: s.label_lqi === true,
+27 -3
View File
@@ -364,7 +364,7 @@ export const cardStyles = css`
pointer-events: none; /* draggable only in plan mode (rule below) */
position: absolute;
transform: translate(-50%, -50%);
font-size: calc(var(--icon-size, 2.5cqw) * 0.5 * var(--rl-scale, 1) * var(--rl-font, 1));
font-size: calc(var(--icon-size, 2.5cqw) * 0.5 * var(--rl-scale, 1) * var(--rl-font, 1) * var(--rl-space, 1));
font-weight: 700;
letter-spacing: 0.04em;
white-space: nowrap;
@@ -377,7 +377,12 @@ export const cardStyles = css`
gap: 0.15em;
text-align: center;
}
.rlname { display: inline-flex; align-items: center; gap: 0.25em; }
.rlname {
display: inline-flex;
align-items: center;
gap: 0.25em;
font-size: calc(1em * var(--rl-name, 1));
}
.rlgear {
--mdc-icon-size: 0.9em;
display: inline-flex;
@@ -401,7 +406,7 @@ export const cardStyles = css`
display: flex;
align-items: center;
gap: 0.55em;
font-size: 0.62em;
font-size: calc(0.62em * var(--rl-meta, 1));
font-weight: 600;
letter-spacing: 0.02em;
opacity: 0.9;
@@ -469,6 +474,25 @@ export const cardStyles = css`
.ctrlstate { display: inline-flex; align-items: center; gap: 5px; color: var(--hp-muted); }
.ctrlstate.on { color: var(--hp-txt); }
.ctrlstate ha-icon { --mdc-icon-size: 15px; }
.cardpreview {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
margin: 8px 0 2px;
padding: 10px;
border: 1px dashed var(--hp-muted);
border-radius: 8px;
}
.cardpreview .cpname { font-weight: 700; letter-spacing: 0.04em; }
.cardpreview .cpmeta {
display: inline-flex;
align-items: center;
gap: 0.3em;
font-weight: 600;
opacity: 0.85;
}
.cardpreview .cpmeta ha-icon { --mdc-icon-size: 1.05em; }
.iconauto {
display: flex;
align-items: center;
+3
View File
@@ -10,6 +10,9 @@ export interface RoomCfg {
/** 'device:<id>' or 'entity:<eid>'; unset = average over the room sensors. */
temp_source?: string | null;
hum_source?: string | null;
/** Font multipliers for THIS room's card (0.5-3, unset = 1). */
name_scale?: number | null;
label_scale?: number | null;
} | null;
id?: string;
name: string;