mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
feat(plan): device markers are landmarks in the Plan editor
The Plan editor hid every device marker (display: none), so walls, openings and stairs were placed against devices from memory. It now shows them exactly as the Background editor does (#362): the same markers as View, at the Background editor's effective opacity, fully pointer-inert. The fade is filter: opacity(0.35) on each marker, not on .devlayer, because room labels and room settings buttons share that layer and stay opaque and interactive; filter multiplies the marker's own opacity (.unavail -> 0.35 x 0.35), as the Background layer does. Marker handlers already fail closed outside View/Devices. demo/smoke_plan_device_landmarks.mjs proves AC1/AC2: same markers as View, per-marker alpha equal to Background, core and capsule fall through, a real click on a marker reaches the Walls tool and a room settings button below, cursor and target unchanged, handlers inert, no tabindex. Three mutants guard hiding, opacity override and the pointer boundary. UX-MODES, DECOR-EDITOR, USER-GUIDE.ru mode table, changelogs. Issue: #687 User-Visible: yes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
@@ -14,7 +14,8 @@ const res = await page.evaluate(async () => {
|
||||
out.gearReadable = cs ? parseFloat(cs.fontSize) > 0 && cs.pointerEvents === 'auto' : null;
|
||||
out.gearHasLabel = btn ? btn.textContent.trim().length > 0 : null;
|
||||
const box = btn?.getBoundingClientRect();
|
||||
// (в редакторе плана .dev скрыты display:none — сравнивать не с чем)
|
||||
// (маркеры устройств в редакторе плана — неинтерактивные ориентиры #687,
|
||||
// кнопку с ними не сравниваем)
|
||||
out.gearTapTarget = box ? box.height > 6 && box.height < 40 : null;
|
||||
btn.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||
await c.updateComplete;
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// #687: in the Plan editor device markers are landmarks exactly as in the
|
||||
// Background editor (#362) — the same markers as View, at the Background
|
||||
// editor's effective opacity, and fully pointer-inert so every Plan tool
|
||||
// receives the point below them. Room labels (same .devlayer) stay opaque.
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
const { page, browser } = await launch();
|
||||
|
||||
const probe = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
// Entering a mode settles the camera after the stage resizes; wait until
|
||||
// marker positions hold still so measured points are the clicked points.
|
||||
const layoutKey = () => [...sr().querySelectorAll('.devlayer [data-hp="device"]')]
|
||||
.map((node) => { const r = node.getBoundingClientRect(); return `${r.left.toFixed(1)},${r.top.toFixed(1)}`; })
|
||||
.join('|');
|
||||
const enter = async (mode) => {
|
||||
c._setMode(mode, false);
|
||||
await settleMode();
|
||||
const started = performance.now();
|
||||
let key = layoutKey(), still = 0;
|
||||
while (still < 10 && performance.now() - started < 3000) {
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
const next = layoutKey();
|
||||
still = next === key ? still + 1 : 0;
|
||||
key = next;
|
||||
}
|
||||
await c.updateComplete;
|
||||
};
|
||||
const stage = () => sr().querySelector('.stage');
|
||||
const markers = () => [...sr().querySelectorAll('.devlayer [data-hp="device"]')];
|
||||
const visible = (node) => {
|
||||
const r = node.getBoundingClientRect();
|
||||
return getComputedStyle(node).display !== 'none' && r.width > 0 && r.height > 0;
|
||||
};
|
||||
const visibleIds = () => markers().filter(visible).map((node) => node.dataset.id).sort();
|
||||
// Effective alpha of one marker: its own and every ancestor's opacity and
|
||||
// filter: opacity(), up to and including the stage.
|
||||
const effectiveOpacity = (node) => {
|
||||
let alpha = 1;
|
||||
for (let el = node; el; el = el.parentElement) {
|
||||
const cs = getComputedStyle(el);
|
||||
alpha *= Number(cs.opacity);
|
||||
for (const m of cs.filter.matchAll(/opacity\(([^)]+)\)/g)) alpha *= Number(m[1]);
|
||||
if (el === stage()) break;
|
||||
}
|
||||
return alpha;
|
||||
};
|
||||
const opacityById = () => Object.fromEntries(markers().filter(visible)
|
||||
.map((node) => [node.dataset.id, effectiveOpacity(node)]));
|
||||
|
||||
// One unavailable marker exercises contract item 2: the fade multiplies
|
||||
// the marker's own opacity (.unavail) exactly as the Background layer does.
|
||||
const target = markers().find((node) => node.dataset.entity && c.hass.states[node.dataset.entity]);
|
||||
if (target) {
|
||||
const entity = target.dataset.entity;
|
||||
c.hass = { ...c.hass, states: { ...c.hass.states,
|
||||
[entity]: { ...c.hass.states[entity], state: 'unavailable' } } };
|
||||
await c.updateComplete;
|
||||
}
|
||||
await enter('view');
|
||||
out.unavailableMarkerInView = !!sr().querySelector('.devlayer [data-hp="device"].unavail');
|
||||
const viewIds = visibleIds();
|
||||
out.viewHasMarkers = viewIds.length > 0;
|
||||
await enter('decor');
|
||||
const decorOpacity = opacityById();
|
||||
await enter('plan');
|
||||
const planIds = visibleIds();
|
||||
const planOpacity = opacityById();
|
||||
|
||||
// AC1 — the same markers as View, each at the Background editor's alpha.
|
||||
out.planShowsViewMarkers = JSON.stringify(planIds) === JSON.stringify(viewIds);
|
||||
out.planOpacityMatchesBackground = planIds.length > 0 && planIds.every((id) =>
|
||||
id in decorOpacity && Math.abs(planOpacity[id] - decorOpacity[id]) < 1e-3);
|
||||
const plain = markers().find((node) => visible(node) && !node.classList.contains('unavail'));
|
||||
out.planPlainMarkerAt35 = !!plain && Math.abs(planOpacity[plain.dataset.id] - 0.35) < 1e-3;
|
||||
const unavail = markers().find((node) => visible(node) && node.classList.contains('unavail'));
|
||||
out.planUnavailableMarkerAt35x35 = !!unavail
|
||||
&& Math.abs(planOpacity[unavail.dataset.id] - 0.35 * 0.35) < 1e-3;
|
||||
// Room labels share .devlayer: no layer fade reaches them (their own
|
||||
// opacity is the label design, not this contract).
|
||||
const label = sr().querySelector('.roomlabel');
|
||||
out.roomLabelNotFaded = !!label
|
||||
&& Math.abs(effectiveOpacity(label) - Number(getComputedStyle(label).opacity)) < 1e-3
|
||||
&& !/opacity\(/.test(getComputedStyle(label).filter);
|
||||
out.planMarkersOutOfTabOrder = markers().every((node) => !node.hasAttribute('tabindex')
|
||||
&& !node.hasAttribute('role'));
|
||||
|
||||
// AC2 — pick a marker wholly inside the stage, preferring a value capsule
|
||||
// that extends outside the marker's nominal box.
|
||||
const s = stage().getBoundingClientRect();
|
||||
const inside = markers().filter((node) => {
|
||||
const r = node.getBoundingClientRect();
|
||||
return visible(node) && r.left > s.left + 4 && r.right < s.right - 4
|
||||
&& r.top > s.top + 4 && r.bottom < s.bottom - 4;
|
||||
});
|
||||
const at = (p) => p ? sr().elementFromPoint(p.x, p.y) : null;
|
||||
const centre = (node) => {
|
||||
const b = node.getBoundingClientRect();
|
||||
return { x: b.left + b.width / 2, y: b.top + b.height / 2 };
|
||||
};
|
||||
// Room chrome lying under a marker keeps the hit (the demo plan puts a
|
||||
// marker over a room's settings button).
|
||||
const roomChrome = '.roomlabel, [data-hp="room-settings"]';
|
||||
const overLabel = inside.find((node) => at(centre(node))?.closest?.(roomChrome));
|
||||
out.roomChromeUnderMarkerGetsHit = !!overLabel;
|
||||
const gearPoint = overLabel && at(centre(overLabel))?.closest?.('[data-hp="room-settings"]')
|
||||
? centre(overLabel) : null;
|
||||
// The tool probe lies over bare plan, so the Walls tool owns the point;
|
||||
// prefer a value capsule that extends outside the marker's nominal box.
|
||||
const overPlan = inside.filter((node) => !at(centre(node))?.closest?.('.devlayer'));
|
||||
const dev = overPlan.find((node) => node.classList.contains('valonly')
|
||||
|| node.querySelector('.device-shell.with-values')) || overPlan[0];
|
||||
const frame = dev?.querySelector('.device-shell-frame');
|
||||
const r = dev?.getBoundingClientRect();
|
||||
const fr = frame?.getBoundingClientRect();
|
||||
const core = dev ? centre(dev) : null;
|
||||
const capsule = fr && fr.right > r.right + 2
|
||||
? { x: fr.right - 1, y: fr.top + fr.height / 2 } : core;
|
||||
out.probeFound = !!dev && !!core;
|
||||
out.probeHasOutsideCapsule = !!fr && fr.right > r.right + 2;
|
||||
const cursorAt = (p) => { const t = at(p); return t ? getComputedStyle(t).cursor : null; };
|
||||
const hit = { core: at(core), capsule: at(capsule) };
|
||||
const cursor = { core: cursorAt(core), capsule: cursorAt(capsule) };
|
||||
out.coreFallsThrough = !!hit.core && !hit.core.closest('.dev');
|
||||
out.capsuleFallsThrough = !!hit.capsule && !hit.capsule.closest('.dev');
|
||||
out.markerSubtreeIsPointerInert = !!dev && !!frame
|
||||
&& getComputedStyle(dev).pointerEvents === 'none'
|
||||
&& getComputedStyle(dev, '::before').pointerEvents === 'none'
|
||||
&& getComputedStyle(frame).pointerEvents === 'none'
|
||||
&& [...dev.querySelectorAll('*')].every((el) => getComputedStyle(el).pointerEvents === 'none');
|
||||
// "The cursor over a marker is the cursor of what lies below": the same
|
||||
// point with the marker taken out of layout gives the same target/cursor.
|
||||
if (dev) {
|
||||
dev.style.display = 'none';
|
||||
out.targetSameWithoutMarker = at(core) === hit.core && at(capsule) === hit.capsule;
|
||||
out.cursorSameWithoutMarker = cursorAt(core) === cursor.core && cursorAt(capsule) === cursor.capsule;
|
||||
dev.style.display = '';
|
||||
}
|
||||
|
||||
c._tool = 'draw'; c._path = []; c._cursorPt = null;
|
||||
await c.updateComplete;
|
||||
return { out, core, id: dev?.dataset.id, gearPoint };
|
||||
});
|
||||
|
||||
// A real mouse click on the marker core reaches the Walls tool. It runs
|
||||
// before the synthetic probes below, which leave pointer state behind.
|
||||
if (probe.core) await page.mouse.click(probe.core.x, probe.core.y);
|
||||
|
||||
// A real click on a marker lying over a room's settings button opens the
|
||||
// room dialog: the button below owns the point.
|
||||
if (probe.gearPoint) await page.mouse.click(probe.gearPoint.x, probe.gearPoint.y);
|
||||
const gear = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
await c.updateComplete;
|
||||
const opened = c._roomDialog === true && !!c._roomEditId;
|
||||
if (opened) { c._roomDialogCancel(); await c.updateComplete; }
|
||||
return { roomSettingsOpenThroughMarker: opened };
|
||||
});
|
||||
|
||||
const inert = await page.evaluate(async ({ core, id }) => {
|
||||
const out = {};
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
await c.updateComplete;
|
||||
out.wallsToolReceivesClickThroughMarker = c._path.length === 1;
|
||||
c._path = []; c._cursorPt = null;
|
||||
await c.updateComplete;
|
||||
const dev = [...sr().querySelectorAll('.devlayer [data-hp="device"]')].find((node) => node.dataset.id === id);
|
||||
const r = dev?.getBoundingClientRect();
|
||||
// The click above must have landed on the probed marker.
|
||||
out.probeDidNotMove = !!r && !!core
|
||||
&& Math.abs(r.left + r.width / 2 - core.x) < 1 && Math.abs(r.top + r.height / 2 - core.y) < 1;
|
||||
// Handlers fail closed even when events are dispatched on the marker itself.
|
||||
let serviceCalls = 0, wsCalls = 0;
|
||||
const oldCallService = c.hass.callService;
|
||||
const oldCallWS = c.hass.callWS;
|
||||
c.hass.callService = () => { serviceCalls += 1; };
|
||||
c.hass.callWS = async () => { wsCalls += 1; return { ok: true }; };
|
||||
const before = { tip: c._tip, info: c._infoCard, drag: c._deviceDrag, sel: c._selId };
|
||||
const opts = { bubbles: true, composed: true, cancelable: true, clientX: core?.x || 0, clientY: core?.y || 0 };
|
||||
dev?.dispatchEvent(new PointerEvent('pointerover', { ...opts, pointerId: 687, pointerType: 'mouse', isPrimary: true }));
|
||||
dev?.dispatchEvent(new PointerEvent('pointermove', { ...opts, pointerId: 687, pointerType: 'mouse', isPrimary: true }));
|
||||
dev?.dispatchEvent(new PointerEvent('pointerdown', { ...opts, pointerId: 688, pointerType: 'mouse', button: 0, isPrimary: true }));
|
||||
dev?.dispatchEvent(new PointerEvent('pointerup', { ...opts, pointerId: 688, pointerType: 'mouse', button: 0, isPrimary: true }));
|
||||
dev?.dispatchEvent(new MouseEvent('click', { ...opts, button: 0 }));
|
||||
const ctx = new MouseEvent('contextmenu', { ...opts, button: 2 });
|
||||
dev?.dispatchEvent(ctx);
|
||||
await c.updateComplete;
|
||||
out.handlersFailClosed = serviceCalls === 0 && wsCalls === 0
|
||||
&& c._tip === before.tip && c._infoCard === before.info
|
||||
&& c._deviceDrag === before.drag && c._selId === before.sel;
|
||||
out.noDeviceHoverAttribute = !sr().querySelector('.dev[data-hp-device-hover]');
|
||||
out.contextMenuNotClaimed = !ctx.defaultPrevented;
|
||||
c.hass.callService = oldCallService;
|
||||
c.hass.callWS = oldCallWS;
|
||||
c._pointers?.clear?.(); c._panStart = null; c._panLock = null;
|
||||
c._path = []; c._cursorPt = null;
|
||||
await c.updateComplete;
|
||||
return out;
|
||||
}, { core: probe.core, id: probe.id });
|
||||
|
||||
const res = { ...probe.out, ...gear, ...inert };
|
||||
checkAll(res);
|
||||
await finish(browser, res);
|
||||
@@ -6,6 +6,11 @@
|
||||
focus frame when it receives focus; it can still be reached with Tab and
|
||||
followed with Enter or Space
|
||||
([#686](https://github.com/Matysh/houseplan-card/issues/686)).
|
||||
- The Plan editor now shows device markers as translucent landmarks, exactly as
|
||||
the Background editor does: the same markers as View at 35% opacity. They
|
||||
never react to clicks, hover or the cursor, so every Plan tool, room label and
|
||||
room settings button works through them
|
||||
([#687](https://github.com/Matysh/houseplan-card/issues/687)).
|
||||
|
||||
## v1.78.0-beta.7 — 2026-09-28
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
фокуса браузера; до неё по-прежнему можно дойти клавишей Tab и перейти по
|
||||
Enter или Пробелу
|
||||
([#686](https://github.com/Matysh/houseplan-card/issues/686)).
|
||||
- Редактор плана теперь показывает значки устройств полупрозрачными ориентирами —
|
||||
так же, как редактор подложки: те же значки, что в просмотре, с
|
||||
непрозрачностью 35 %. Они не реагируют на клики, наведение и курсор, поэтому
|
||||
все инструменты плана, подписи комнат и кнопки настроек комнат работают сквозь
|
||||
них ([#687](https://github.com/Matysh/houseplan-card/issues/687)).
|
||||
|
||||
## v1.78.0-beta.7 — 2026-09-28
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ light routing, device state or Home Assistant actions.
|
||||
| Scale | Corner drag preserves aspect ratio. Hold `Shift` for independent axes. Furniture and custom images also have four one-axis middle handles and may cross the fixed edge to mirror. |
|
||||
| Rotation | Ordinary decor uses 5° steps by default and `Shift` for free rotation. Furniture and custom images are free by default and `Shift` snaps to 45°. Lines use endpoint handles instead of a rotation handle. |
|
||||
| Magnet targets | Only other decor objects and room contours: corners, edge centres, centres and edges. The image, devices and openings are excluded. |
|
||||
| Context emphasis | Decor and its editing chrome stay fully opaque. Rooms, labels, devices, openings, positive-thickness walls and solid/dashed zero-thickness walls are contextual only and render at 35% opacity. The whole device presentation (core, ring, capsule, values and badges) is pointer-inert: it never hovers, opens, acts or drags, and the active Background tool receives a press through it. |
|
||||
| Context emphasis | Decor and its editing chrome stay fully opaque. Rooms, labels, devices, openings, positive-thickness walls and solid/dashed zero-thickness walls are contextual only and render at 35% opacity. The whole device presentation (core, ring, capsule, values and badges) is pointer-inert: it never hovers, opens, acts or drags, and the active Background tool receives a press through it. The Plan editor shows devices the same way, as a per-marker 35% fade because room labels share the layer there (#687, `UX-MODES.md` › Plan). |
|
||||
| View composition | All decor kinds form one layer above room/data fills, room hover fill, opening-tunnel fills and Glow base. Live Glow, sun, physical walls, opening symbols, devices and room labels remain above decor. The plan image remains below it (§3.3). |
|
||||
| Compatibility | Legacy `width`, text `size/scale` and `plan_scale` remain readable. New writes use `width_cm`, `size_cm` and `plan_scale_x/y`. |
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ Escape сначала отменяет активный диалог, меню,
|
||||
| Режим | Основное назначение | Устройства | Комнаты/стены | Декор и проёмы |
|
||||
|---|---|---|---|---|
|
||||
| Просмотр | Контроль дома | Интерактивны; видны состояния, значения и подсказки | Комната подсвечивается при наведении; подсказка показывает название, чистую площадь, температуру, влажность и LQI | Видны, если не скрыты настройками пространства; проёмы не реагируют на клик, кроме значка замка |
|
||||
| Редактор плана | Геометрия комнат, стен и проёмов | Скрыты | Интерактивны; видны кнопки настроек комнат, размеры и сетка | Проёмы всегда видны и редактируются; стены нулевой толщины рисуются полностью по оси |
|
||||
| Редактор плана | Геометрия комнат, стен и проёмов | Не редактируются и полупрозрачны, как в редакторе подложки; клики и наведение проходят к инструментам плана | Интерактивны; видны кнопки настроек комнат, размеры и сетка | Проёмы всегда видны и редактируются; стены нулевой толщины рисуются полностью по оси |
|
||||
| Редактор устройств | Позиции и параметры маркеров | Перетаскиваются; клик открывает настройки; доступны скрытые маркеры | Не редактируются | Служат фоном |
|
||||
| Редактор подложки | Изображение, линии, фигуры, текст, мебель | Не редактируются и полупрозрачны | Комнаты и все стены полупрозрачны | Декор интерактивен и непрозрачен; скрытый в просмотре декор всё равно виден здесь |
|
||||
| Киоск | Настенный экран | Интерактивны как в просмотре | Только отображение | Только отображение; редакторы отсутствуют |
|
||||
|
||||
@@ -172,6 +172,11 @@ tabs, the editor-close reserve, zoom and one gear whose menu holds the rest.
|
||||
editor with the draw tool armed (an empty floor has nothing useful in View).
|
||||
- ⚙ General settings (fill palette) lives here — it is about the plan's appearance.
|
||||
|
||||
- Devices are landmarks here exactly as in Background (#687, #362): the same
|
||||
markers as View at 35% (each marker's own opacity multiplied, as the
|
||||
Background layer does) and fully pointer-inert — every press, hover and
|
||||
cursor belongs to the Plan tool, room label or room settings button below.
|
||||
Room labels share the device layer and stay opaque and interactive.
|
||||
- Independent partitions and columns are masonry for hit testing as well as
|
||||
area/light: room hover stops at their physical bodies just as it stops at a
|
||||
thick room wall. This does not split the room or change its HA area.
|
||||
|
||||
@@ -3955,6 +3955,51 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' && !inEditorSecondary) {',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'plan-device-landmarks-hidden',
|
||||
guard: 'node demo/smoke_plan_device_landmarks.mjs',
|
||||
because: '#687 AC1: the Plan editor shows the same markers as View as landmarks; hiding '
|
||||
+ 'them again removes the reference the owner asked for',
|
||||
patches: [{
|
||||
file: 'src/styles/plan.styles.ts',
|
||||
find: ' .stage.markup .devlayer .dev {\n'
|
||||
+ ' filter: opacity(0.35);\n'
|
||||
+ ' }\n',
|
||||
replace: ' .stage.markup .devlayer .dev {\n'
|
||||
+ ' display: none;\n'
|
||||
+ ' }\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'plan-device-landmarks-override-own-opacity',
|
||||
guard: 'node demo/smoke_plan_device_landmarks.mjs',
|
||||
because: '#687 contract item 2: the fade multiplies the marker\'s own opacity as the '
|
||||
+ 'Background layer does; an opacity override shows an unavailable marker at 35% '
|
||||
+ 'instead of 35% x 35%',
|
||||
patches: [{
|
||||
file: 'src/styles/plan.styles.ts',
|
||||
find: ' .stage.markup .devlayer .dev {\n'
|
||||
+ ' filter: opacity(0.35);\n',
|
||||
replace: ' .stage.markup .devlayer .dev {\n'
|
||||
+ ' opacity: 0.35;\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'plan-device-landmarks-hit-target',
|
||||
guard: 'node demo/smoke_plan_device_landmarks.mjs',
|
||||
because: '#687 AC2: a visible marker must never own a Plan-editor point; without the '
|
||||
+ 'pointer boundary its 44 px hit area and capsule swallow the Walls tool click and the '
|
||||
+ 'room settings button below',
|
||||
patches: [{
|
||||
file: 'src/styles/plan.styles.ts',
|
||||
find: ' .stage.markup .devlayer .dev,\n'
|
||||
+ ' .stage.markup .devlayer .dev *,\n'
|
||||
+ ' .stage.markup .devlayer .dev::before {\n'
|
||||
+ ' pointer-events: none;\n'
|
||||
+ ' }\n',
|
||||
replace: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'opening-light-quantum-identity',
|
||||
guard: 'node --test --test-name-pattern="#366" test/logic.test.mjs',
|
||||
|
||||
@@ -1176,8 +1176,20 @@ export const planStyles = css`
|
||||
.stage.markup .room {
|
||||
pointer-events: none;
|
||||
}
|
||||
/* #687: devices are landmarks in the Plan editor too, exactly as in
|
||||
Background (#362): the same markers as View, at the same 35% and fully
|
||||
pointer-inert, so every Plan tool receives the point below them. The
|
||||
fade sits on each marker, not on .devlayer: room labels share that
|
||||
layer and stay opaque and interactive here. filter: opacity()
|
||||
multiplies the marker's own opacity (.unavail), as the Background
|
||||
layer opacity does. */
|
||||
.stage.markup .devlayer .dev {
|
||||
display: none; /* in plan mode the icons do not get in the way; labels stay */
|
||||
filter: opacity(0.35);
|
||||
}
|
||||
.stage.markup .devlayer .dev,
|
||||
.stage.markup .devlayer .dev *,
|
||||
.stage.markup .devlayer .dev::before {
|
||||
pointer-events: none;
|
||||
}
|
||||
/* mode frames: the edit modes are visible at a glance */
|
||||
.stage.mode-plan {
|
||||
|
||||
Reference in New Issue
Block a user