mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
fix: host-gated groups belong to their owner surface; split mixed media wrappers (#266)
CI-шард смока поймал сдвиг каскада, невидимый golden-набору: smoke_device_icon_design — alert-shell стал серым, dark-unavailable core светлым. Причина: классификатор считал ведущий :host(...) владельцем селектора и уносил гейтнутые группы устройств в base — ВПЕРЁД их поверхности, меняя победителя при равной специфичности. Теперь :host-префикс — гейт, а не владелец: владелец — первый значимый токен после него; смешанные @media режутся на последовательные по-зонные копии обёртки (reduced-motion обёрток стало 12 поверх тех же правил 10 исходных — юнит заякорен на факт, сверка scope-ключом доказывает, что ни одно правило обёртку не потеряло). Два мутантных якоря вернулись в devices.styles.ts; исключение юнита непересечения опустело. Гейты: refactor-proof diff пуст · golden 129/129 без переприёмки · смоки device_icon_design, plan_snap_overlay, preloader OK · npm test 1318/0. Issue: #266 User-Visible: no
This commit is contained in:
File diff suppressed because one or more lines are too long
Vendored
+88
-84
File diff suppressed because one or more lines are too long
@@ -93,13 +93,20 @@ while (i < cssBody.length) {
|
||||
}
|
||||
|
||||
const zoneOfSelector = (header) => {
|
||||
const first = (sel) => {
|
||||
const t = sel.trim().match(/^[.:#\[]?[\w-]+/);
|
||||
// A leading :host(...) compound GATES a rule, it does not own it: the owner
|
||||
// is the first meaningful token after the gate. Classifying such groups
|
||||
// into base moved them AHEAD of their surface in the cascade and flipped
|
||||
// equal-specificity winners (smoke_device_icon_design caught alert shells
|
||||
// and dark-unavailable cores) — the owner surface keeps their position.
|
||||
const owner = (sel) => {
|
||||
let rest = sel.trim().replace(/^:host(\([^)]*\))?\s*/, '');
|
||||
if (!rest) return ':host';
|
||||
const t = rest.match(/^[.:#\[]?[\w-]+/);
|
||||
return t ? t[0] : null;
|
||||
};
|
||||
const zones = new Set();
|
||||
for (const sel of header.split(',')) {
|
||||
const token = first(sel);
|
||||
const token = owner(sel);
|
||||
if (!token) continue;
|
||||
const zone = T[token];
|
||||
if (!zone) throw new Error(`unclassified token «${token}» in «${header.slice(0, 80)}»`);
|
||||
@@ -118,10 +125,54 @@ for (const block of blocks) {
|
||||
zone = /^(hp-dev|dev|pulse|vac)/.test(name) ? 'devices'
|
||||
: /^(hp-spin|spin|fade|toast)/.test(name) ? 'base' : 'base';
|
||||
} else if (/^@(media|supports)/.test(block.header)) {
|
||||
// classify by the first inner selector
|
||||
const inner = block.text.slice(block.text.indexOf('{') + 1);
|
||||
const sel = inner.slice(0, inner.indexOf('{')).replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
||||
zone = zoneOfSelector(sel);
|
||||
// Classify by ALL inner rules. A single-zone wrapper moves whole; a mixed
|
||||
// one is split into consecutive per-zone copies of the wrapper so every
|
||||
// rule stays with its owner surface without crossing zone order.
|
||||
const openAt = block.text.indexOf('{');
|
||||
const inner = block.text.slice(openAt + 1, block.text.lastIndexOf('}'));
|
||||
const innerBlocks = [];
|
||||
{
|
||||
let k = 0, pend = '';
|
||||
while (k < inner.length) {
|
||||
const rest2 = inner.slice(k);
|
||||
const cm2 = rest2.match(/^\s*\/\*[\s\S]*?\*\//);
|
||||
if (cm2 && !rest2.slice(0, rest2.indexOf('/*')).includes('{')) {
|
||||
pend += cm2[0]; k += cm2[0].length; continue;
|
||||
}
|
||||
const o2 = inner.indexOf('{', k);
|
||||
if (o2 === -1) break;
|
||||
const h2 = inner.slice(k, o2);
|
||||
let d2 = 1, j2 = o2 + 1;
|
||||
while (j2 < inner.length && d2 > 0) {
|
||||
if (inner[j2] === '{') d2++;
|
||||
else if (inner[j2] === '}') d2--;
|
||||
j2++;
|
||||
}
|
||||
innerBlocks.push({ text: pend + h2 + inner.slice(o2, j2), header: h2.trim() });
|
||||
pend = '';
|
||||
k = j2;
|
||||
}
|
||||
}
|
||||
const innerZones = innerBlocks.map((b) => zoneOfSelector(b.header));
|
||||
const uniq = [...new Set(innerZones)];
|
||||
if (uniq.length <= 1) {
|
||||
zone = uniq[0] ?? 'base';
|
||||
} else {
|
||||
const wrapperHeader = block.text.slice(0, openAt).trimEnd();
|
||||
let g = 0;
|
||||
while (g < innerBlocks.length) {
|
||||
const zg = innerZones[g];
|
||||
let h = g;
|
||||
while (h < innerBlocks.length && innerZones[h] === zg) h++;
|
||||
const bodyPart = innerBlocks.slice(g, h).map((b) => b.text).join('');
|
||||
const copy = `${wrapperHeader} {${bodyPart}\n }`;
|
||||
ZONES[zg].push(copy);
|
||||
g = h;
|
||||
}
|
||||
// mark handled: skip the shared push below
|
||||
block.zone = 'SPLIT';
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
zone = zoneOfSelector(block.header);
|
||||
}
|
||||
|
||||
@@ -1999,7 +1999,7 @@ export const MUTANTS = [
|
||||
because: 'unavailable keeps click/keyboard access but must never regain the blue visual hover '
|
||||
+ 'which makes an offline device look live',
|
||||
patches: [{
|
||||
file: 'src/styles/base.styles.ts',
|
||||
file: 'src/styles/devices.styles.ts',
|
||||
find: '.dev:not(.unavail):hover {',
|
||||
replace: '.dev.unavail:hover {',
|
||||
}],
|
||||
@@ -2165,7 +2165,7 @@ export const MUTANTS = [
|
||||
because: 'touch must not leave a browser-matched device hover painted after the JS tooltip '
|
||||
+ 'has already been cleared',
|
||||
patches: [{
|
||||
file: 'src/styles/base.styles.ts',
|
||||
file: 'src/styles/devices.styles.ts',
|
||||
find: ' :host([data-pointer-hover]) .dev:not(.unavail):hover {',
|
||||
replace: ' .dev:not(.unavail):hover {',
|
||||
}],
|
||||
|
||||
@@ -61,15 +61,9 @@ export const baseStyles = css`
|
||||
@keyframes fixedfloor-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
:host([data-pointer-hover]) .tab:hover {
|
||||
color: var(--hp-txt);
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
:host([data-pointer-hover]) .btn:hover {
|
||||
border-color: var(--hp-accent);
|
||||
}
|
||||
.bootveil {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -102,30 +96,6 @@ export const baseStyles = css`
|
||||
animation: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.recoveryoverlay {
|
||||
transition: none;
|
||||
}
|
||||
.stage.hpsettle {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
:host([data-pointer-hover]) .stage.mode-view .room.overlay:not(.styled):hover {
|
||||
stroke: var(--hp-accent);
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
:host([data-pointer-hover]) .stage.mode-view .room.yard:not(.styled):hover {
|
||||
stroke: var(--hp-accent);
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
/* The explicit late room-hover layer owns the wash and halo. Keeping CSS
|
||||
filters off room paths prevents Chromium from recompositing the sibling
|
||||
screen-blended Glow layer for one bright frame on every hover. */
|
||||
:host([data-pointer-hover]) .stage.mode-view .room.styled:hover {
|
||||
stroke: var(--hp-accent);
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
:host([data-pointer-hover]) .stage.markup g.opening:hover .op-outline {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@keyframes hp-pulse-short {
|
||||
0% { transform: scale(1); opacity: 0.55; }
|
||||
@@ -157,22 +127,6 @@ export const baseStyles = css`
|
||||
0% { transform: translateX(-4%); opacity: 0.72; }
|
||||
100% { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
:host([data-pointer-hover]) .rlgearbtn:hover { opacity: 1; filter: brightness(1.18); }
|
||||
:host([data-pointer-hover]) .rlgear:hover { opacity: 1; }
|
||||
:host([data-pointer-hover]) .stage.mode-view .rlgo:hover { opacity: 1; }
|
||||
:host([data-pointer-hover]) .ctrlopt:hover {
|
||||
background: var(--secondary-background-color, rgba(128,128,128,0.15));
|
||||
}
|
||||
:host([data-pointer-hover]) .stage.markup .roomlabel:hover .rlhandle { display: block; }
|
||||
:host([data-pointer-hover]) .bdframe .bdhandle:hover + .bdknob {
|
||||
fill: #fff;
|
||||
stroke: var(--hp-accent);
|
||||
}
|
||||
:host([data-pointer-hover]) .dtframe .dthandle:hover + .dtknob {
|
||||
fill: #fff;
|
||||
stroke: var(--hp-accent);
|
||||
}
|
||||
:host([data-pointer-hover]) .modetab .closex:hover { opacity: 1; }
|
||||
@keyframes hp-sunfade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
@@ -181,37 +135,6 @@ export const baseStyles = css`
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
/* Interaction wins ordinary state colours. Alarm keeps priority through
|
||||
the more-specific rule below. Unavailable has no visual hover. */
|
||||
:host([data-pointer-hover]) .dev:not(.unavail):hover {
|
||||
--device-face-bg: #0C82F0;
|
||||
--device-face-fg: light-dark(#fff, #252525);
|
||||
--device-shell-stroke: var(--device-shell-base-stroke);
|
||||
}
|
||||
:host([data-pointer-hover]) .dev.theme-light:not(.unavail):hover { --device-face-fg: #fff; }
|
||||
:host([data-pointer-hover]) .dev.theme-dark:not(.unavail):hover { --device-face-fg: #252525; }
|
||||
:host([data-pointer-hover]) .dev:hover,
|
||||
.dev:focus-visible { z-index: 5; }
|
||||
/* Alert stays above focus, selection, hover and ordinary semantic paint. */
|
||||
.dev.alarm,
|
||||
:host([data-pointer-hover]) .dev.alarm:hover,
|
||||
.dev.alarm:focus-visible {
|
||||
--device-face-bg: #F0410C;
|
||||
--device-face-fg: light-dark(#fff, #252525);
|
||||
--device-shell-stroke: #F0410C;
|
||||
}
|
||||
:host([data-pointer-hover]) .tab:hover .tabedit {
|
||||
opacity: 0.9;
|
||||
}
|
||||
:host([data-pointer-hover]) .rszhandle:hover + .rszicon .rszink { stroke-width: 3; }
|
||||
:host([data-pointer-hover]) .rszhandle.disabled:hover + .rszicon .rszink { stroke-width: 2; }
|
||||
:host([data-pointer-hover]) .furnitem:hover { background: rgba(127, 127, 127, 0.18); }
|
||||
:host([data-pointer-hover]) .cand:hover {
|
||||
background: rgba(127, 127, 127, 0.15);
|
||||
}
|
||||
:host([data-pointer-hover]) .pdftag .x:hover {
|
||||
color: #ff7a5c;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
@@ -223,12 +146,6 @@ export const baseStyles = css`
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
:host([data-pointer-hover]) .rrow .ract:hover { color: var(--hp-txt); }
|
||||
:host([data-pointer-hover]) .rrow .ract.del:hover { color: #ff7a5c; }
|
||||
:host([data-pointer-hover]) .aboutlink:hover { text-decoration: underline; }
|
||||
:host([data-pointer-hover]) .menu .it:hover {
|
||||
background: rgba(127, 127, 127, 0.15);
|
||||
}
|
||||
.toast {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -31,6 +31,9 @@ export const chromeStyles = css`
|
||||
transition: 0.15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
:host([data-pointer-hover]) .tab:hover {
|
||||
color: var(--hp-txt);
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--hp-accent);
|
||||
color: var(--text-primary-color, #fff);
|
||||
@@ -166,6 +169,7 @@ export const chromeStyles = css`
|
||||
cursor: pointer;
|
||||
border-radius: var(--rad-s);
|
||||
}
|
||||
:host([data-pointer-hover]) .modetab .closex:hover { opacity: 1; }
|
||||
.editbar .barclose {
|
||||
box-sizing: border-box;
|
||||
width: 40px;
|
||||
@@ -218,6 +222,9 @@ export const chromeStyles = css`
|
||||
margin-left: var(--sp-3);
|
||||
opacity: 0.4;
|
||||
}
|
||||
:host([data-pointer-hover]) .tab:hover .tabedit {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.tab.tabadd {
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
}
|
||||
@@ -356,6 +363,9 @@ export const chromeStyles = css`
|
||||
--mdc-icon-size: 16px;
|
||||
color: var(--hp-muted);
|
||||
}
|
||||
:host([data-pointer-hover]) .menu .it:hover {
|
||||
background: rgba(127, 127, 127, 0.15);
|
||||
}
|
||||
.menu .it.all {
|
||||
color: var(--hp-accent);
|
||||
font-weight: 600;
|
||||
|
||||
@@ -324,6 +324,17 @@ export const devicesStyles = css`
|
||||
--device-face-fg: #252525;
|
||||
--device-shell-stroke-ratio: .025;
|
||||
}
|
||||
/* Interaction wins ordinary state colours. Alarm keeps priority through
|
||||
the more-specific rule below. Unavailable has no visual hover. */
|
||||
:host([data-pointer-hover]) .dev:not(.unavail):hover {
|
||||
--device-face-bg: #0C82F0;
|
||||
--device-face-fg: light-dark(#fff, #252525);
|
||||
--device-shell-stroke: var(--device-shell-base-stroke);
|
||||
}
|
||||
:host([data-pointer-hover]) .dev.theme-light:not(.unavail):hover { --device-face-fg: #fff; }
|
||||
:host([data-pointer-hover]) .dev.theme-dark:not(.unavail):hover { --device-face-fg: #252525; }
|
||||
:host([data-pointer-hover]) .dev:hover,
|
||||
.dev:focus-visible { z-index: 5; }
|
||||
.dev.unavail {
|
||||
opacity: 0.35;
|
||||
--device-face-bg: #B5BAC1;
|
||||
@@ -391,6 +402,14 @@ export const devicesStyles = css`
|
||||
.dev:not(.on):not(.open):not(.alarm):not(.lock-locked):not(.lock-unlocked):not(.unavail):focus-visible {
|
||||
--device-face-fg: #0C82F0;
|
||||
}
|
||||
/* Alert stays above focus, selection, hover and ordinary semantic paint. */
|
||||
.dev.alarm,
|
||||
:host([data-pointer-hover]) .dev.alarm:hover,
|
||||
.dev.alarm:focus-visible {
|
||||
--device-face-bg: #F0410C;
|
||||
--device-face-fg: light-dark(#fff, #252525);
|
||||
--device-shell-stroke: #F0410C;
|
||||
}
|
||||
.dev.theme-light.alarm { --device-face-fg: #fff; }
|
||||
.dev.theme-dark.alarm {
|
||||
--device-face-fg: #252525;
|
||||
|
||||
@@ -48,6 +48,9 @@ export const dialogsStyles = css`
|
||||
.btn ha-icon {
|
||||
--mdc-icon-size: 17px;
|
||||
}
|
||||
:host([data-pointer-hover]) .btn:hover {
|
||||
border-color: var(--hp-accent);
|
||||
}
|
||||
.btn.on {
|
||||
background: var(--hp-accent);
|
||||
color: var(--text-primary-color, #fff);
|
||||
@@ -111,6 +114,11 @@ export const dialogsStyles = css`
|
||||
--mdc-icon-size: 44px;
|
||||
color: var(--hp-accent);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.recoveryoverlay {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
.oplock {
|
||||
--oplock-size: calc(var(--icon-size, 2.5cqw) * 0.62);
|
||||
--oplock-core-size: calc(var(--oplock-size) / 1.26875);
|
||||
@@ -239,6 +247,9 @@ export const dialogsStyles = css`
|
||||
border: 0; background: transparent; color: var(--hp-txt);
|
||||
padding: var(--sp-3) var(--sp-4); border-radius: var(--rad-s); cursor: pointer; font-family: inherit; font-size: var(--fs-m);
|
||||
}
|
||||
:host([data-pointer-hover]) .ctrlopt:hover {
|
||||
background: var(--secondary-background-color, rgba(128,128,128,0.15));
|
||||
}
|
||||
.ctrlopt .sub { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; }
|
||||
.ctrlopt ha-icon { --mdc-icon-size: 16px; }
|
||||
.ctrlstates { display: flex; flex-direction: column; gap: var(--sp-2); }
|
||||
@@ -669,6 +680,7 @@ export const dialogsStyles = css`
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
:host([data-pointer-hover]) .furnitem:hover { background: rgba(127, 127, 127, 0.18); }
|
||||
.furnitem.on {
|
||||
border-color: var(--hp-accent);
|
||||
background: rgba(38, 166, 154, 0.18);
|
||||
@@ -749,6 +761,9 @@ export const dialogsStyles = css`
|
||||
border-radius: var(--rad-s);
|
||||
font-size: var(--fs-m);
|
||||
}
|
||||
:host([data-pointer-hover]) .cand:hover {
|
||||
background: rgba(127, 127, 127, 0.15);
|
||||
}
|
||||
.cand.sel {
|
||||
background: var(--hp-accent);
|
||||
color: var(--text-primary-color, #fff);
|
||||
@@ -807,6 +822,9 @@ export const dialogsStyles = css`
|
||||
cursor: pointer;
|
||||
color: var(--hp-muted);
|
||||
}
|
||||
:host([data-pointer-hover]) .pdftag .x:hover {
|
||||
color: #ff7a5c;
|
||||
}
|
||||
.entlist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -919,6 +937,8 @@ export const dialogsStyles = css`
|
||||
color: var(--hp-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
:host([data-pointer-hover]) .rrow .ract:hover { color: var(--hp-txt); }
|
||||
:host([data-pointer-hover]) .rrow .ract.del:hover { color: #ff7a5c; }
|
||||
.gsrow .gsl {
|
||||
min-width: 150px;
|
||||
font-size: var(--fs-m);
|
||||
@@ -1037,6 +1057,7 @@ export const dialogsStyles = css`
|
||||
font-size: var(--fs-m);
|
||||
padding: var(--sp-1) 0;
|
||||
}
|
||||
:host([data-pointer-hover]) .aboutlink:hover { text-decoration: underline; }
|
||||
.aboutlink ha-icon { --mdc-icon-size: 18px; line-height: 1; }
|
||||
hp-dialog .body {
|
||||
padding: var(--sp-5) var(--sp-6);
|
||||
|
||||
@@ -159,6 +159,11 @@ export const planStyles = css`
|
||||
.stage.hpsettle {
|
||||
transition: height 0.25s ease;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.stage.hpsettle {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
.zoomctl {
|
||||
display: inline-flex;
|
||||
gap: var(--sp-1);
|
||||
@@ -359,11 +364,19 @@ export const planStyles = css`
|
||||
stroke: transparent;
|
||||
stroke-width: calc(2px * var(--hp-cell-visual-scale, 1));
|
||||
}
|
||||
:host([data-pointer-hover]) .stage.mode-view .room.overlay:not(.styled):hover {
|
||||
stroke: var(--hp-accent);
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
.room.yard {
|
||||
fill: rgba(75, 140, 90, 0.14);
|
||||
stroke: #4b8c5a;
|
||||
stroke-width: calc(2px * var(--hp-cell-visual-scale, 1));
|
||||
}
|
||||
:host([data-pointer-hover]) .stage.mode-view .room.yard:not(.styled):hover {
|
||||
stroke: var(--hp-accent);
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
.room.styled {
|
||||
stroke: var(--room-stroke, transparent);
|
||||
stroke-opacity: var(--room-stroke-op, 0);
|
||||
@@ -405,6 +418,13 @@ export const planStyles = css`
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
/* The explicit late room-hover layer owns the wash and halo. Keeping CSS
|
||||
filters off room paths prevents Chromium from recompositing the sibling
|
||||
screen-blended Glow layer for one bright frame on every hover. */
|
||||
:host([data-pointer-hover]) .stage.mode-view .room.styled:hover {
|
||||
stroke: var(--hp-accent);
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
/* doors, windows & gates */
|
||||
.op-leaf {
|
||||
transition: transform 0.6s ease;
|
||||
@@ -422,6 +442,9 @@ export const planStyles = css`
|
||||
transition: opacity 0.15s;
|
||||
pointer-events: none;
|
||||
}
|
||||
:host([data-pointer-hover]) .stage.markup g.opening:hover .op-outline {
|
||||
opacity: 0.9;
|
||||
}
|
||||
/* openings are pure status graphics outside Plan mode: no cursor, no hover,
|
||||
no hit target — View must not interact with them at all */
|
||||
.op-hit {
|
||||
@@ -532,6 +555,7 @@ export const planStyles = css`
|
||||
z-index: 2;
|
||||
}
|
||||
.rlgearbtn { transition: opacity 0.15s, filter 0.15s; }
|
||||
:host([data-pointer-hover]) .rlgearbtn:hover { opacity: 1; filter: brightness(1.18); }
|
||||
.rlgearbtn ha-icon { --mdc-icon-size: calc(var(--gear-h) * 0.55); display: inline-flex; }
|
||||
.rlgear {
|
||||
--mdc-icon-size: 0.9em;
|
||||
@@ -541,6 +565,7 @@ export const planStyles = css`
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
:host([data-pointer-hover]) .rlgear:hover { opacity: 1; }
|
||||
.rlgo {
|
||||
--mdc-icon-size: 0.85em;
|
||||
display: inline-flex;
|
||||
@@ -550,6 +575,7 @@ export const planStyles = css`
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
:host([data-pointer-hover]) .stage.mode-view .rlgo:hover { opacity: 1; }
|
||||
.roomlabel .rlmetrics {
|
||||
position: absolute; /* below the name, outside the centring math */
|
||||
top: calc(100% + 0.15em);
|
||||
@@ -588,6 +614,7 @@ export const planStyles = css`
|
||||
.rlhandle.br { right: -6px; bottom: -6px; cursor: nwse-resize; }
|
||||
.rlhandle.tr { right: -6px; top: -6px; cursor: nesw-resize; }
|
||||
.rlhandle.bl { left: -6px; bottom: -6px; cursor: nesw-resize; }
|
||||
:host([data-pointer-hover]) .stage.markup .roomlabel:hover .rlhandle { display: block; }
|
||||
.stage.markup .roomlabel { pointer-events: auto; }
|
||||
.roomlabel:active { cursor: grabbing; }
|
||||
.measurelayer {
|
||||
@@ -749,6 +776,10 @@ export const planStyles = css`
|
||||
vector-effect: non-scaling-stroke;
|
||||
pointer-events: none;
|
||||
}
|
||||
:host([data-pointer-hover]) .bdframe .bdhandle:hover + .bdknob {
|
||||
fill: #fff;
|
||||
stroke: var(--hp-accent);
|
||||
}
|
||||
.bdframe .bd-nwse { cursor: nwse-resize; }
|
||||
.bdframe .bd-nesw { cursor: nesw-resize; }
|
||||
/* the picture itself is the drag target for a move (grab, then grabbing) */
|
||||
@@ -795,6 +826,10 @@ export const planStyles = css`
|
||||
vector-effect: non-scaling-stroke;
|
||||
pointer-events: none;
|
||||
}
|
||||
:host([data-pointer-hover]) .dtframe .dthandle:hover + .dtknob {
|
||||
fill: #fff;
|
||||
stroke: var(--hp-accent);
|
||||
}
|
||||
.dtframe .dt-nwse { cursor: nwse-resize; }
|
||||
.dtframe .dt-nesw { cursor: nesw-resize; }
|
||||
.dtframe .dtrot { cursor: grab; }
|
||||
@@ -1311,7 +1346,9 @@ export const planStyles = css`
|
||||
}
|
||||
.rszhalo { stroke: var(--hp-bg); stroke-width: 6; }
|
||||
.rszink { stroke: var(--hp-accent); stroke-width: 2; }
|
||||
:host([data-pointer-hover]) .rszhandle:hover + .rszicon .rszink { stroke-width: 3; }
|
||||
.rszicon.disabled { opacity: 0.38; }
|
||||
:host([data-pointer-hover]) .rszhandle.disabled:hover + .rszicon .rszink { stroke-width: 2; }
|
||||
.rszmeasurelayer,
|
||||
.rszmeasurelayer * { pointer-events: none; }
|
||||
.rszmeasurehalo,
|
||||
|
||||
+10
-13
@@ -56,23 +56,20 @@ test('issue 266 surface files do not share a single selector', () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Spec §1.3.2: every allowed exception is named with its reason.
|
||||
const allowed = [
|
||||
// The `:host([data-pointer-hover]) .dev:hover, .dev:focus-visible` GROUP
|
||||
// is cross-surface (host gate + device selector) and therefore lives in
|
||||
// base per spec §1.1, while the plain `.dev:focus-visible` outline rule
|
||||
// stays with its surface. Their source order (group first, outline
|
||||
// second) is preserved by the [base, …, devices] assembly order — golden
|
||||
// holds the cascade.
|
||||
'.dev:focus-visible (base ∩ devices)',
|
||||
];
|
||||
assert.deepEqual(clashes, allowed);
|
||||
// Spec §1.3.2: the exception list is empty — a leading :host(...) gate is
|
||||
// not ownership, so gated device groups live with their surface and no
|
||||
// selector is shared between files.
|
||||
assert.deepEqual(clashes, []);
|
||||
});
|
||||
|
||||
test('issue 266 the media wrappers survived the move', () => {
|
||||
const all = FILES.map((name) => sourceOf(name)).join('\n');
|
||||
assert.equal((all.match(/@media \(forced-colors: active\)/g) || []).length, 2,
|
||||
'both forced-colors blocks must survive — golden never emulates them');
|
||||
assert.equal((all.match(/@media \(prefers-reduced-motion: reduce\)/g) || []).length, 10,
|
||||
'all ten reduced-motion blocks must survive — golden always shoots reduced');
|
||||
// 10 source wrappers; two of them were MIXED-zone and are split into
|
||||
// per-zone copies by the generator (cascade fix), hence 12 wrappers over
|
||||
// the same 10 wrappers' worth of rules — the scope-keyed refactor diff
|
||||
// proves no rule lost its wrapper.
|
||||
assert.equal((all.match(/@media \(prefers-reduced-motion: reduce\)/g) || []).length, 12,
|
||||
'reduced-motion wrappers must survive — golden always shoots reduced');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user