mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
room settings button: room-centred, icon-sized, zooms with the plan
Owner's spec: the button is no longer glued to the room NAME (which the user can drag anywhere) — it anchors to the geometric centre of the ROOM (interiorPoint for polygons, so an L-shaped room gets a point actually inside it), one button-height below centre so it never covers the name, whose default position is that same centre. Height is 70% of a device icon box, and since --icon-size already rescales with the view, the button zooms with the plan instead of keeping a constant screen size (verified: x2.2 zoom -> x2.20 button). The small metric rows under the room name (temperature, humidity, signal, lights) now render in the plan editor too — they used to be view-mode only. smoke_room_cards updated: plainInPlan now asserts metrics ARE present in the editor (the old assertion pinned the old behaviour), plus gearDetached.
This commit is contained in:
@@ -36,8 +36,14 @@ const res = await page.evaluate(async () => {
|
|||||||
c._setMode('plan'); await c.updateComplete;
|
c._setMode('plan'); await c.updateComplete;
|
||||||
const planLbl = sr().querySelector('.roomlabel');
|
const planLbl = sr().querySelector('.roomlabel');
|
||||||
out.handlesInPlan = planLbl?.querySelectorAll('.rlhandle').length === 4;
|
out.handlesInPlan = planLbl?.querySelectorAll('.rlhandle').length === 4;
|
||||||
// метрики скрыты в редакторе плана (карточка = имя, чтобы не мешать разметке)
|
// метрики видны и в редакторе плана (решение владельца, 2026-07-29)
|
||||||
out.plainInPlan = sr().querySelectorAll('.roomlabel.card').length === 0;
|
out.plainInPlan = sr().querySelectorAll('.roomlabel.card').length > 0;
|
||||||
|
// кнопка настроек — отдельно от подписи, в центре комнаты, с размером от иконки
|
||||||
|
out.gearDetached = (() => {
|
||||||
|
const g = sr().querySelector('.rlgearbtn');
|
||||||
|
if (!g || g.closest('.roomlabel')) return false;
|
||||||
|
return getComputedStyle(g).height !== 'auto';
|
||||||
|
})();
|
||||||
// масштаб: сымитируем resize через прямой вызов
|
// масштаб: сымитируем resize через прямой вызов
|
||||||
const room = c._spaceModel().rooms.find((r) => r.name);
|
const room = c._spaceModel().rooms.find((r) => r.name);
|
||||||
c._rlResize = { id: 'rl_' + room.id, space: spId, k0: 1, cx: 100, cy: 100, d0: 50 };
|
c._rlResize = { id: 'rl_' + room.id, space: spId, k0: 1, cx: 100, cy: 100, d0: 50 };
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Vendored
+25
-16
File diff suppressed because one or more lines are too long
@@ -239,6 +239,11 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
|||||||
on the plan after a reload. Same for each tap action and each fill mode
|
on the plan after a reload. Same for each tap action and each fill mode
|
||||||
[auto: backend test_every_display_mode_the_editor_offers_is_accepted and
|
[auto: backend test_every_display_mode_the_editor_offers_is_accepted and
|
||||||
neighbours, test_a_marker_showing_its_value_can_be_saved]
|
neighbours, test_a_marker_showing_its_value_can_be_saved]
|
||||||
|
- [ ] Room settings button (dev): detached from the (movable) name label —
|
||||||
|
always at the room's geometric centre, one button-height below it; sized
|
||||||
|
at 70% of a device icon and zooming WITH the plan; the small metric rows
|
||||||
|
under the room name now show in the plan editor too
|
||||||
|
[auto: smoke_room_cards gearDetached/plainInPlan]
|
||||||
- [ ] Hide-from-plan flag (dev, docs/FILTERING.md): every device dialog has
|
- [ ] Hide-from-plan flag (dev, docs/FILTERING.md): every device dialog has
|
||||||
the checkbox, incl. virtual; hidden devices vanish from every mode and
|
the checkbox, incl. virtual; hidden devices vanish from every mode and
|
||||||
the count, still count toward room LQI, cast no glow/light fill; the
|
the count, still count toward room LQI, cast no glow/light fill; the
|
||||||
|
|||||||
+25
-10
@@ -15,7 +15,7 @@ import {
|
|||||||
lqiColor, snapToGrid, samePoint, pointInPolygon, markerIdForBinding,
|
lqiColor, snapToGrid, samePoint, pointInPolygon, markerIdForBinding,
|
||||||
segmentCm, formatLength, roomEdges, roomPoly, pointStrictlyInside, roomsOverlap,
|
segmentCm, formatLength, roomEdges, roomPoly, pointStrictlyInside, roomsOverlap,
|
||||||
pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf, sharedBoundary, openZoneOf, distToSegment, outlineWithout, cutSegments, alignGuides, segmentAngle, is45, type AlignGuide, swipeTarget, clampScale, migratePdfUrls, roomFillModeOf, contentUrl,
|
pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf, sharedBoundary, openZoneOf, distToSegment, outlineWithout, cutSegments, alignGuides, segmentAngle, is45, type AlignGuide, swipeTarget, clampScale, migratePdfUrls, roomFillModeOf, contentUrl,
|
||||||
snapToWall, openingAmount,
|
snapToWall, openingAmount, interiorPoint,
|
||||||
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
||||||
stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, glowColorOf, doorSector, hasRoomBehind, controlsAction, isControllable,
|
stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, glowColorOf, doorSector, hasRoomBehind, controlsAction, isControllable,
|
||||||
spaceDisplayOf, roomFillStyle, fillColorsOf, DEFAULT_FILL_COLORS, type FillColors,
|
spaceDisplayOf, roomFillStyle, fillColorsOf, DEFAULT_FILL_COLORS, type FillColors,
|
||||||
@@ -4157,6 +4157,7 @@ class HouseplanCard extends LitElement {
|
|||||||
${disp.showNames || this._markup
|
${disp.showNames || this._markup
|
||||||
? space.rooms.map((r) => this._renderRoomLabel(r, space, view, disp))
|
? space.rooms.map((r) => this._renderRoomLabel(r, space, view, disp))
|
||||||
: nothing}
|
: nothing}
|
||||||
|
${this._markup ? space.rooms.map((r) => this._renderRoomGear(r, space, view)) : nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._measureAnchor
|
${this._measureAnchor
|
||||||
? html`<div class="measurelayer">${this._renderMeasureLabel(view)}</div>`
|
? html`<div class="measurelayer">${this._renderMeasureLabel(view)}</div>`
|
||||||
@@ -4501,6 +4502,28 @@ class HouseplanCard extends LitElement {
|
|||||||
this._persistLayout();
|
this._persistLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The room-settings button: detached from the (movable) label, always at
|
||||||
|
* the geometric CENTRE of the room, sized at 70% of a device icon and
|
||||||
|
* therefore zooming with the plan (owner's spec, 2026-07-29). */
|
||||||
|
private _renderRoomGear(
|
||||||
|
r: RoomCfg, space: SpaceModel, view: { x: number; y: number; w: number; h: number },
|
||||||
|
): TemplateResult | typeof nothing {
|
||||||
|
if (!r.id) return nothing;
|
||||||
|
const c = r.poly
|
||||||
|
? (interiorPoint(r.poly) || [r.poly[0][0], r.poly[0][1]])
|
||||||
|
: r.x != null && r.y != null ? [r.x + (r.w || 0) / 2, r.y + (r.h || 0) / 2] : null;
|
||||||
|
if (!c) return nothing;
|
||||||
|
const left = ((c[0] - view.x) / view.w) * 100;
|
||||||
|
const top = ((c[1] - view.y) / view.h) * 100;
|
||||||
|
return html`<button class="rlgearbtn" style="left:${left}%;top:${top}%"
|
||||||
|
title=${this._t('room.settings_title')}
|
||||||
|
@pointerdown=${(e: Event) => e.stopPropagation()}
|
||||||
|
@click=${(e: Event) => { e.stopPropagation(); this._openRoomEdit(r); }}>
|
||||||
|
<ha-icon icon="mdi:cog-outline"></ha-icon>
|
||||||
|
<span class="rlgeartext">${this._t('room.settings_short')}</span>
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
private _renderRoomLabel(
|
private _renderRoomLabel(
|
||||||
r: RoomCfg, space: SpaceModel, view: { x: number; y: number; w: number; h: number }, disp: SpaceDisplay,
|
r: RoomCfg, space: SpaceModel, view: { x: number; y: number; w: number; h: number }, disp: SpaceDisplay,
|
||||||
): TemplateResult | typeof nothing {
|
): TemplateResult | typeof nothing {
|
||||||
@@ -4514,7 +4537,7 @@ class HouseplanCard extends LitElement {
|
|||||||
const k = this._labelScale(r);
|
const k = this._labelScale(r);
|
||||||
// optional metrics row (needs an HA area; sub-area rooms show the name only)
|
// optional metrics row (needs an HA area; sub-area rooms show the name only)
|
||||||
const rows: TemplateResult[] = [];
|
const rows: TemplateResult[] = [];
|
||||||
if ((r.area || r.settings?.temp_source || r.settings?.hum_source) && !this._markup) {
|
if (r.area || r.settings?.temp_source || r.settings?.hum_source) {
|
||||||
if (disp.labelTemp) {
|
if (disp.labelTemp) {
|
||||||
const t = this._roomTemp(r);
|
const t = this._roomTemp(r);
|
||||||
if (t != null) rows.push(html`<span class="rlm"><ha-icon icon="mdi:thermometer"></ha-icon>${t}°</span>`);
|
if (t != null) rows.push(html`<span class="rlm"><ha-icon icon="mdi:thermometer"></ha-icon>${t}°</span>`);
|
||||||
@@ -4552,14 +4575,6 @@ class HouseplanCard extends LitElement {
|
|||||||
@pointerdown=${(e: Event) => e.stopPropagation()}></ha-icon>`
|
@pointerdown=${(e: Event) => e.stopPropagation()}></ha-icon>`
|
||||||
: nothing}</span>
|
: nothing}</span>
|
||||||
${rows.length ? html`<span class="rlmetrics">${rows}</span>` : nothing}
|
${rows.length ? html`<span class="rlmetrics">${rows}</span>` : nothing}
|
||||||
${this._markup && r.id
|
|
||||||
? html`<button class="rlgearbtn" title=${this._t('room.settings_title')}
|
|
||||||
@pointerdown=${(e: Event) => e.stopPropagation()}
|
|
||||||
@click=${(e: Event) => { e.stopPropagation(); this._openRoomEdit(r); }}>
|
|
||||||
<ha-icon icon="mdi:cog-outline"></ha-icon>
|
|
||||||
<span class="rlgeartext">${this._t('room.settings_short')}</span>
|
|
||||||
</button>`
|
|
||||||
: nothing}
|
|
||||||
${this._mode === 'plan'
|
${this._mode === 'plan'
|
||||||
? ['tl', 'tr', 'bl', 'br'].map(
|
? ['tl', 'tr', 'bl', 'br'].map(
|
||||||
(c) => html`<span class="rlhandle ${c}"
|
(c) => html`<span class="rlhandle ${c}"
|
||||||
|
|||||||
+15
-7
@@ -403,31 +403,39 @@ export const cardStyles = css`
|
|||||||
and the metrics hang below as absolutes — the name renders in exactly
|
and the metrics hang below as absolutes — the name renders in exactly
|
||||||
the same place in view mode and in the plan editor (owner's request),
|
the same place in view mode and in the plan editor (owner's request),
|
||||||
and the button sits at the very bottom of the card. */
|
and the button sits at the very bottom of the card. */
|
||||||
|
/* Standalone, centred on the room, sized from the device icon: icon-size
|
||||||
|
already rescales with the view, so the button zooms WITH the plan
|
||||||
|
instead of keeping a constant screen size (owner's spec). */
|
||||||
.rlgearbtn {
|
.rlgearbtn {
|
||||||
|
--gear-h: calc(var(--icon-size, 2.5cqw) * 0.7 * 2.2); /* 70% of the icon BOX (icon + padding) */
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 0.35em);
|
/* anchored to the room centre, sitting one button-height below it: the
|
||||||
left: 50%;
|
room NAME defaults to the same centre, and dead-centred the button
|
||||||
transform: translateX(-50%);
|
covered it. The offset is in button units, so it zooms along. */
|
||||||
|
transform: translate(-50%, calc(-50% + var(--gear-h) * 0.95));
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 0.35em;
|
||||||
padding: 3px 8px;
|
height: var(--gear-h);
|
||||||
|
padding: 0 calc(var(--gear-h) * 0.38);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--hp-accent);
|
background: var(--hp-accent);
|
||||||
color: var(--text-primary-color, #fff);
|
color: var(--text-primary-color, #fff);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 11px;
|
font-size: calc(var(--gear-h) * 0.42);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
opacity: 0.92;
|
opacity: 0.92;
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.rlgearbtn { transition: opacity 0.15s, filter 0.15s; }
|
.rlgearbtn { transition: opacity 0.15s, filter 0.15s; }
|
||||||
.rlgearbtn:hover { opacity: 1; filter: brightness(1.18); }
|
.rlgearbtn:hover { opacity: 1; filter: brightness(1.18); }
|
||||||
.rlgearbtn ha-icon { --mdc-icon-size: 14px; display: inline-flex; }
|
.rlgearbtn ha-icon { --mdc-icon-size: calc(var(--gear-h) * 0.55); display: inline-flex; }
|
||||||
.rlgear {
|
.rlgear {
|
||||||
--mdc-icon-size: 0.9em;
|
--mdc-icon-size: 0.9em;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user