fix: unify lock and orange icon colors

Issue: #219
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-08-20 14:56:21 +03:00
parent 84fa434a64
commit bc75e00c2d
16 changed files with 229 additions and 143 deletions
File diff suppressed because one or more lines are too long
+8 -1
View File
@@ -10,7 +10,14 @@ mkdirSync(artifactDir, { recursive: true });
const referenceAsset = (theme, file, coreSize) => {
let source = readFileSync(resolve('demo/srv/reference/device-icons', theme, file), 'utf8');
if (theme === 'Dark' && file === 'Unlock.svg') source = source.replaceAll('#1DC21D', '#F0A00C');
if (file === 'Lock.svg') {
const old = theme === 'Dark' ? '#252525' : 'black';
source = source.replaceAll(old, '#66D17A');
if (theme === 'Dark') source = source.replaceAll('fill="white"', 'fill="#252525"');
}
if (file === 'Unlock.svg') {
source = source.replaceAll(theme === 'Dark' ? '#1DC21D' : '#F0A00C', '#F0410C');
}
const nativeWidth = Number(source.match(/<svg[^>]*width="([\d.]+)"/)?.[1] || 127);
return {
url: `data:image/svg+xml;base64,${Buffer.from(source).toString('base64')}`,
+1
View File
@@ -242,6 +242,7 @@ export const GOLDEN_SCENARIOS = Object.freeze([
'golden-light-three': ['lock-unlocked'],
'golden-presence': ['virtual'],
'golden-climate': ['alarm', 'sel'],
'golden-right-linkquality': ['open'],
},
focusDevice: 'golden-climate',
hoverDevice: 'golden-presence',
+46 -10
View File
@@ -55,13 +55,20 @@ const reference = {
alert: svgState('Dark', 'Alert Value.svg'),
},
};
// Owner decision in #179 overrides the stale green Dark/Unlock asset.
reference.dark.unlock = {
...reference.dark.unlock,
core: reference.light.unlock.core,
glyph: reference.dark.active.glyph,
shell: reference.light.unlock.shell,
};
// Owner decision in #219 supersedes the package paint while retaining its
// geometry: a closed lock is green, an open lock is red, and the glyph follows
// the same Light/Dark foreground as every other coloured core.
for (const theme of ['light', 'dark']) {
const glyph = theme === 'light' ? browserColor('#FFFFFF') : browserColor('#252525');
reference[theme].lock = {
...reference[theme].lock,
core: browserColor('#66D17A'), glyph, shell: browserColor('#66D17A'),
};
reference[theme].unlock = {
...reference[theme].unlock,
core: browserColor('#F0410C'), glyph, shell: browserColor('#F0410C'),
};
}
const { page, browser } = await launch(
{ width: 1120, height: 900 }, 1, [], { colorScheme: 'dark' },
@@ -320,6 +327,7 @@ const result = await page.evaluate(async ({ beforeUnavailable, afterUnavailable,
const stateMatrix = Object.fromEntries(['light', 'dark'].map((theme) => [theme, {
default: sampleState(theme),
active: sampleState(theme, ['on']),
open: sampleState(theme, ['open']),
lock: sampleState(theme, ['lock-locked']),
unlock: sampleState(theme, ['lock-unlocked']),
alert: sampleState(theme, ['alarm']),
@@ -329,6 +337,22 @@ const result = await page.evaluate(async ({ beforeUnavailable, afterUnavailable,
lockNode.className = originalLockClasses;
lockNode.setAttribute('style', originalLockStyle);
const openingProbe = document.createElement('div');
openingProbe.innerHTML = '<span class="oplock-shell"><span class="oplock-core">'
+ '<ha-icon icon="mdi:lock"></ha-icon></span></span>';
sr().append(openingProbe);
const sampleOpeningLock = (theme, state) => {
openingProbe.className = `oplock theme-${theme} ${state}`;
const core = getComputedStyle(openingProbe.querySelector('.oplock-core'));
const shell = getComputedStyle(openingProbe.querySelector('.oplock-shell'));
return { core: core.backgroundColor, glyph: core.color, shell: shell.borderColor };
};
const openingLockMatrix = Object.fromEntries(['light', 'dark'].map((theme) => [theme, {
lock: sampleOpeningLock(theme, 'locked'),
unlock: sampleOpeningLock(theme, 'unlocked'),
}]));
openingProbe.remove();
const originalLightStyle = light.getAttribute('style') || '';
const originalDoubleStyle = double.getAttribute('style') || '';
const originalTextStyle = text.getAttribute('style') || '';
@@ -490,6 +514,7 @@ const result = await page.evaluate(async ({ beforeUnavailable, afterUnavailable,
defaultShell,
focusCoreDecoration,
stateMatrix,
openingLockMatrix,
sizeMatrix,
textShellSizeMatrix,
textFrameRadius,
@@ -520,6 +545,15 @@ const result = await page.evaluate(async ({ beforeUnavailable, afterUnavailable,
&& stateMatrix[theme][state].shell === reference[theme][state].shell
&& Math.abs(stateMatrix[theme][state].shellWidth
- Math.max(1, Math.floor(reference[theme][state].shellWidth * 50.4))) <= 0.5)),
openingLockPaletteMatches: ['light', 'dark'].every((theme) =>
['lock', 'unlock'].every((state) =>
openingLockMatrix[theme][state].core === reference[theme][state].core
&& openingLockMatrix[theme][state].glyph === reference[theme][state].glyph
&& openingLockMatrix[theme][state].shell === reference[theme][state].shell)),
orangeGlyphsFollowTheme: stateMatrix.light.active.glyph === 'rgb(255, 255, 255)'
&& stateMatrix.light.open.glyph === 'rgb(255, 255, 255)'
&& stateMatrix.dark.active.glyph === 'rgb(37, 37, 37)'
&& stateMatrix.dark.open.glyph === 'rgb(37, 37, 37)',
referenceHoverMatches: lightHover.core === reference.light.hover.core
&& lightHover.glyph === reference.light.hover.glyph
&& lightHover.shell === reference.light.default.shell
@@ -575,9 +609,11 @@ const result = await page.evaluate(async ({ beforeUnavailable, afterUnavailable,
keyboardActionFeedback,
informationalActionHasNoFeedback,
lightThemeCoreIsWhite: lightThemeCore === 'rgb(255, 255, 255)',
darkThemeCoreIs252525: darkThemeClassProjected && darkThemeCore === 'rgb(37, 37, 37)',
darkLockUsesWhiteGlyphAndDarkShell: darkLockGlyph === 'rgb(255, 255, 255)'
&& darkLockShell === 'rgb(37, 37, 37)',
darkThemeCoreIs252525: darkThemeClassProjected
&& stateMatrix.dark.default.core === 'rgb(37, 37, 37)',
darkLockUsesGreenCoreAndDarkGlyph: darkThemeCore === 'rgb(102, 209, 122)'
&& darkLockGlyph === 'rgb(37, 37, 37)'
&& darkLockShell === 'rgb(102, 209, 122)',
hitAreaAtLeast44: parseFloat(pseudo.width) >= 44 && parseFloat(pseudo.height) >= 44,
presenceContinuousGreen: presence?.querySelector('.device-pulse.continuous.reason-presence')
&& getComputedStyle(presence.querySelector('.device-pulse i')).borderColor === 'rgb(29, 194, 29)'
File diff suppressed because one or more lines are too long
+5 -2
View File
@@ -7,8 +7,11 @@ for issues #179 and #211. They do not ship in the production bundle.
- Export: 2026-08-19
- Source archive SHA-256:
`63670C73E25D1E59DDAF1BE236F3D7F2FAC827B9B5D6DD4B77125EA9BC012025`
- Owner override: Dark Unlock is amber, not the stale green paint in
`Dark/Unlock.svg`.
- Owner override #219: package geometry remains normative, but Lock/Unlock
paint is projected at runtime as closed green `#66D17A` and open red
`#F0410C`; glyphs are white in Light and `#252525` in Dark. The SVG files stay
byte-identical source evidence, while the smoke and comparison capture apply
this explicit product override.
`demo/smoke_device_icon_design.mjs` reads the SVG attributes directly for the
computed-style contract. `demo/capture_device_icon_reference.mjs` builds a
+18 -24
View File
File diff suppressed because one or more lines are too long
+4
View File
@@ -2,6 +2,10 @@
## Unreleased
- Door and gate locks now use green for locked and red for unlocked on both
compact opening badges and ordinary device markers. Every glyph on an orange
device core is now white in the light theme and dark in the dark theme
([#219](https://github.com/Matysh/houseplan-card/issues/219)).
- Fixed Glow disappearing from an entire space when harmless floating-point
tails or one malformed room outline upset floor clipping. Boolean inputs are
now stabilised at render time, and a residual failure is isolated to the
+5
View File
@@ -8,6 +8,11 @@
## Не выпущено
- Замки дверей и ворот теперь зелёные в закрытом состоянии и красные в
открытом — и на компактном значке проёма, и на обычном маркере устройства.
Все иконки на оранжевой подложке устройства теперь белые в светлой теме и
тёмные в тёмной
([#219](https://github.com/Matysh/houseplan-card/issues/219)).
- Исправлено исчезновение Glow во всём пространстве из-за безвредных хвостов
вычислений с плавающей точкой или одного некорректного контура комнаты.
Входные данные для геометрических операций теперь стабилизируются только при
+31 -13
View File
@@ -361,7 +361,8 @@ public binding-status hook.
The independent reference subset under
`demo/srv/reference/device-icons/` comes directly from designer package 1.1.1;
it is not generated from production CSS. The package archive hash and the
owner's amber Dark Unlock override are recorded in that directory's README.
owner's #219 red/green Lock/Unlock paint override are recorded in that
directory's README.
- [ ] `node demo/smoke_device_icon_design.mjs` reads the SVG colors and stroke
widths, then compares them with fresh computed styles. It also measures
@@ -403,6 +404,20 @@ owner's amber Dark Unlock override are recorded in that directory's README.
- [ ] `node demo/capture_device_icon_reference.mjs` includes an additional
96 px Text row beside the normative Light/Dark `Text Default.svg`.
## Device lock and orange foreground palette (#219)
- [ ] Closed/`locked` is green `#66D17A`; open/`unlocked` is red `#F0410C`.
The same core/stroke palette is used by ordinary lock markers and compact
door/gate lock badges; glyph shape remains closed/open/question.
- [ ] Every device glyph on an orange core (`on`/working and physical `open`)
is white in Light and `#252525` in Dark. `device-icon-state-table-light`
and `device-icon-state-table-dark` show `on` and `open` together, plus
both lock states [unit: device-marker-polish-contract; auto:
smoke_device_icon_design; golden: device-icon-state-table-*].
- [ ] Alarm, hover, focus, selected, unavailable, virtual, press feedback,
pulse, hit-area and lock actions retain their existing priority and
behaviour [unit: device presentation/polish/pointer; visual source review].
## Touch support and release gates
The product contract is defined in `docs/TOUCH-SUPPORT.md`:
@@ -667,10 +682,11 @@ separately promised workflows:
- [ ] Drag anywhere (no edit mode), snaps to grid, persists after reload, per space
- [ ] ↺ reset restores auto layout after confirm
- [ ] Temperature badge on thermometers; LQI value under zigbee icons with red→green color
- [ ] Unified live states (dev, owner 2026-08-05): actual work is yellow;
open door/window, unlocked lock and open valve are orange; covers stay
neutral and morph their icon; unavailable is faded. The plate and the
activity effect come from the same semantic resolver
- [ ] Unified live states (dev, owner 2026-08-05; lock palette #219): actual
work is yellow; open door/window and open valve are orange; unlocked lock
is red and locked lock is green; covers stay neutral and morph their icon;
unavailable is faded. The plate and the activity effect come from the same
semantic resolver
- [ ] State icons (v1.26.0): auto icons morph with state — door/window/garage open↔closed,
lock locked↔unlocked, bulb on; custom icons and unavailable states never morph [manual]
- [ ] display "Value instead of an icon": the marker shows the measurement (°/%/unit)
@@ -764,10 +780,10 @@ separately promised workflows:
auto-icon family (mdi:roller-shade, mdi:garage-variant), a hand-picked
icon morphs only inside the pair it was picked from, and an
unknown/unavailable state morphs nothing. NOT touched: an open door /
window binary sensor, an unlocked lock and an open valve still wear the
orange «открыто» frame (a valve has no icon pair, so the frame is all it
has) [auto: smoke_cover_no_plate + unit stateIcon «every class, both
ways»]
window binary sensor and an open valve still wear the orange «открыто»
frame (a valve has no icon pair, so the frame is all it has); lock keeps
its separate red-unlocked/green-locked palette [auto:
smoke_cover_no_plate + unit stateIcon «every class, both ways»]
- [ ] Cover target and indication parity: on a device where a hidden functional
`cover.*` competes with an auxiliary option switch, the shared resolver
selects the cover, calls only it and supplies the same entity to icon
@@ -1608,8 +1624,9 @@ require hands on real hardware — they remain for the human pass.
resolved media entities fade only when none is available and powered
- [ ] Controls aggregate their targets: any working target drives both the
yellow plate and the running effect
- [ ] Open contact/unlocked lock/open valve are orange; an open cover stays
neutral because its icon morph carries that state
- [ ] Open contact/open valve are orange; unlocked lock is red and locked lock
is green; an open cover stays neutral because its icon morph carries that
state
- [ ] Unavailable suppresses ordinary activity. Alarm outranks all dynamic
presentation, including when ordinary live states are off; `static_icon`
deliberately hides alarm paint without suppressing service-call errors
@@ -1633,8 +1650,9 @@ require hands on real hardware — they remain for the human pass.
- [ ] Bind a contact sensor: open → leaf swings and the arc draws on in the accent colour;
closed → leaf lies along the wall, arc hidden; invert flips this
- [ ] Sensor unavailable → the opening freezes at its static default (door open / window closed)
- [ ] A door with a lock shows the package padlock badge: black/dark locked,
amber unlocked and neutral unknown in both themes
- [ ] A door with a lock shows the compact padlock badge: green locked, red
unlocked and neutral unknown; coloured glyphs are white in Light and
`#252525` in Dark
- [ ] A gate defaults to 300 cm, has two equal leaves, no swing arc and opens
10° outwards; contact, inversion, lock, drag and resize anchoring match a door
- [ ] Clicking an opening (or the padlock) in view mode opens the info card with both states;
+5 -4
View File
@@ -310,8 +310,8 @@ near-end openings remain visible and are not moved until their geometry is
edited.
An opening may bind a contact; doors and gates may also bind a lock. View paints
the moving leaf and state. A lock badge uses the package's black/dark locked
state and amber unlocked state. A plan tap never toggles a lock. The opening
the moving leaf and state. A lock badge is green when locked and red when
unlocked. A plan tap never toggles a lock. The opening
card provides a labelled
lock/unlock control, with confirmation before unlocking.
@@ -408,8 +408,9 @@ stable core, icon or value, and optional activity pulse. Visual priority is
| State | Meaning | Examples |
|---|---|---|
| Red alarm | Critical condition, even with live states disabled | Smoke, gas, CO, leak, tamper/problem/safety, triggered alarm |
| Yellow | Device is doing its main job or a lock is unlocked | Light/switch/fan on, active climate, vacuum cleaning, known appliance work, unlocked lock |
| Black lock glyph | Lock is secured | Locked lock |
| Yellow | Device is doing its main job | Light/switch/fan on, active climate, vacuum cleaning, known appliance work |
| Red lock | Lock is unsecured | Unlocked/open lock |
| Green lock | Lock is secured | Locked lock |
| Orange | Physically open | Door/window contact, opening valve |
| Faded | Data unavailable | All relevant entities unknown, unavailable or absent |
| Neutral | No alarm, work or open condition | Off, closed, idle, standby, docked |
+6 -5
View File
@@ -523,8 +523,8 @@ YAML-сущность без `unique_id` и строки в Entity Registry: п
### Замок
- `locked` показывается как закрытый чёрный/тёмный замок из дизайн-пакета;
- `unlocked`/`open` — как открытый, с жёлтой подложкой дизайн-пакета;
- `locked` показывается как закрытый замок на зелёной подложке;
- `unlocked`/`open` — как открытый замок на красной подложке;
- неизвестное состояние получает нейтральное/неизвестное оформление;
- клик по значку открывает карточку проёма;
- закрытие выполняется без дополнительного подтверждения;
@@ -775,8 +775,9 @@ Hover включается только после события от наст
| Вид | Что означает | Примеры |
|---|---|---|
| Красная авария | Критическое событие; показывается даже при выключенных живых состояниях | Дым, газ, CO, протечка, tamper/problem/safety, сирена `on`, охранная панель `triggered` |
| Жёлтая подложка | Устройство сейчас выполняет работу либо замок разблокирован | Свет/реле/вентилятор/увлажнитель `on`; climate с распознанным action `heating`, `cooling`, `preheating` или `defrosting`; при отсутствии распознанного action — включённый HVAC-режим как лучшее доступное приближение; vacuum cleaning/returning; script on; техника washing/drying/etc.; lock unlocked/open |
| Чёрный значок замка | Замок заблокирован | lock locked |
| Жёлтая подложка | Устройство сейчас выполняет работу | Свет/реле/вентилятор/увлажнитель `on`; climate с распознанным action `heating`, `cooling`, `preheating` или `defrosting`; при отсутствии распознанного action — включённый HVAC-режим как лучшее доступное приближение; vacuum cleaning/returning; script on; техника washing/drying/etc. |
| Красная подложка замка | Замок разблокирован | lock unlocked/open |
| Зелёная подложка замка | Замок заблокирован | lock locked |
| Оранжевая подложка | Физически открыто | Датчик двери/окна/гаража `on`, клапан open/opening/closing |
| Бледный маркер | Данные недоступны либо медиаплеер выключен | Все рабочие сущности маркера `unknown`, `unavailable` или отсутствуют; `media_player.* = off` |
| Нейтральный | Нет аварии, работы и открытого состояния | Выключено, закрыто, idle/standby/docked и т. п. |
@@ -894,7 +895,7 @@ marker на плане при этом не меняются.
| Door/window/garage contact | Оранжевый, пока `on` | Короткое событие при открытии | Закрыто/открыто для известных классов | Внутренняя карточка |
| `cover.*` штор/жалюзи | Нейтральный | Переход во время opening/closing либо короткий fallback | Закрыто/открыто | Внутренняя карточка; **Переключить состояние** автоматически делает open/close/stop |
| `cover.*` garage/door/gate | Нейтральный | Переход во время движения | Закрыто/открыто | **Переключить состояние** можно сохранить, но оно явно показывает защитный no-op и не выполняет service call |
| `lock.*` | Оранжевый при unlocked/open | Переход при locking/unlocking | Замкнут/открыт | Карточка; Toggle запрещён |
| `lock.*` | Красный при unlocked/open, зелёный при locked | Переход при locking/unlocking | Замкнут/открыт | Карточка; Toggle запрещён |
| `valve.*` | Оранжевый при open/opening/closing | Переход при opening/closing | Обычно нет | Карточка; Toggle можно выбрать явно |
| `climate.*` | Жёлтый при реальном heating/cooling/working; если интеграция не отдаёт action — пока включён HVAC-режим | Работа по `hvac_action`/аналогичному атрибуту, иначе по режиму из `hvac_modes` | Нет | Внутренняя карточка |
| `media_player.*` | Нейтральный при любом включённом/транспортном состоянии; приглушённый при `off`, как при `unknown`/`unavailable` | Нет: `playing`, вспомогательная подсветка и служебные переключатели не считаются работой | Нет | Внутренняя карточка |
+11 -11
View File
@@ -1,7 +1,7 @@
{
"version": 1,
"fixture": "synthetic-only",
"sourceFingerprint": "54e06c66d6c97a941c5b5850da6b71f433ec2f93e75100363c0c943c06e56fce",
"sourceFingerprint": "9c09c069c89daab3481acea8378351def1a80b52790425944cd55b0f1ff621e7",
"captureScriptSha256": "34f2219790d46efd8250e7a1bd829cb8fc0b0547e1260635fefa52407551b41b",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -13,7 +13,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "54e06c66d6c97a941c5b5850da6b71f433ec2f93e75100363c0c943c06e56fce",
"sourceSha256": "9c09c069c89daab3481acea8378351def1a80b52790425944cd55b0f1ff621e7",
"imageSha256": "2885f96e348b15ab7c696e56e99bddcd9bb2ee94218a883e5a2155f45f5042aa"
},
"view-touch": {
@@ -24,7 +24,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "54e06c66d6c97a941c5b5850da6b71f433ec2f93e75100363c0c943c06e56fce",
"sourceSha256": "9c09c069c89daab3481acea8378351def1a80b52790425944cd55b0f1ff621e7",
"imageSha256": "f62d8af3617c00a5e99511bd765980d2e27badf25047a1be34046b64195abe6c"
},
"space-create": {
@@ -35,7 +35,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "54e06c66d6c97a941c5b5850da6b71f433ec2f93e75100363c0c943c06e56fce",
"sourceSha256": "9c09c069c89daab3481acea8378351def1a80b52790425944cd55b0f1ff621e7",
"imageSha256": "c53db2e5c642a5549c13f3c93a5b359fed69bdb2621bf71a243a877ffcb95e6b"
},
"room-contour-close": {
@@ -46,7 +46,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "54e06c66d6c97a941c5b5850da6b71f433ec2f93e75100363c0c943c06e56fce",
"sourceSha256": "9c09c069c89daab3481acea8378351def1a80b52790425944cd55b0f1ff621e7",
"imageSha256": "2f13ea645af306eee9cc7c52699c928377b72f412b08c1a3db6beb848ace1b51"
},
"plan-context-tray": {
@@ -57,7 +57,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "54e06c66d6c97a941c5b5850da6b71f433ec2f93e75100363c0c943c06e56fce",
"sourceSha256": "9c09c069c89daab3481acea8378351def1a80b52790425944cd55b0f1ff621e7",
"imageSha256": "a6c526fede11bc3503fd2384bd4a3f6afa008f481c5c47578f06cae6c81c6f9b"
},
"device-editor": {
@@ -68,7 +68,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "54e06c66d6c97a941c5b5850da6b71f433ec2f93e75100363c0c943c06e56fce",
"sourceSha256": "9c09c069c89daab3481acea8378351def1a80b52790425944cd55b0f1ff621e7",
"imageSha256": "9585b59add4d35b5a6f028ce5b720ef1b77a3f8b19436d192cd1a0e6fc637264"
},
"device-display-preview": {
@@ -79,7 +79,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "54e06c66d6c97a941c5b5850da6b71f433ec2f93e75100363c0c943c06e56fce",
"sourceSha256": "9c09c069c89daab3481acea8378351def1a80b52790425944cd55b0f1ff621e7",
"imageSha256": "cfc317da4628d079a116ff71311fbf06b1eb3b181928a5ea7ba888ab936a5e8b"
},
"background-editor": {
@@ -90,7 +90,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "54e06c66d6c97a941c5b5850da6b71f433ec2f93e75100363c0c943c06e56fce",
"sourceSha256": "9c09c069c89daab3481acea8378351def1a80b52790425944cd55b0f1ff621e7",
"imageSha256": "d7cfe70551d9260169df8efd832e32ed7df99c7f60b55d1fd4a8322bd4c47175"
},
"room-card": {
@@ -101,7 +101,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "54e06c66d6c97a941c5b5850da6b71f433ec2f93e75100363c0c943c06e56fce",
"sourceSha256": "9c09c069c89daab3481acea8378351def1a80b52790425944cd55b0f1ff621e7",
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
},
"device-info": {
@@ -112,7 +112,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "54e06c66d6c97a941c5b5850da6b71f433ec2f93e75100363c0c943c06e56fce",
"sourceSha256": "9c09c069c89daab3481acea8378351def1a80b52790425944cd55b0f1ff621e7",
"imageSha256": "a06cbf83f09e2f67b3566d7c0b10e973db060c3d20786f26f74ada6a21937c3e"
}
}
+17 -23
View File
@@ -831,23 +831,19 @@ export const cardStyles = css`
line-height: 0;
}
.oplock.locked {
--oplock-core-bg: light-dark(#000, #252525);
--oplock-core-fg: #fff;
--oplock-shell-stroke: light-dark(#000, #252525);
}
.oplock.theme-light.locked {
--oplock-core-bg: #000;
--oplock-shell-stroke: #000;
--oplock-core-bg: #66D17A;
--oplock-core-fg: light-dark(#fff, #252525);
--oplock-shell-stroke: #66D17A;
}
.oplock.theme-light.locked { --oplock-core-fg: #fff; }
.oplock.theme-dark.locked {
--oplock-core-bg: #252525;
--oplock-shell-stroke: #252525;
--oplock-core-fg: #252525;
--oplock-stroke-ratio: .025;
}
.oplock.unlocked {
--oplock-core-bg: #F0A00C;
--oplock-core-bg: #F0410C;
--oplock-core-fg: light-dark(#fff, #252525);
--oplock-shell-stroke: #F0A00C;
--oplock-shell-stroke: #F0410C;
}
.oplock.theme-light.unlocked { --oplock-core-fg: #fff; }
.oplock.theme-dark.unlocked {
@@ -2142,26 +2138,24 @@ export const cardStyles = css`
}
.dev.open {
--device-face-bg: var(--hp-open);
--device-face-fg: #4a2800;
--device-face-fg: light-dark(#fff, #252525);
}
.dev.theme-light.open { --device-face-fg: #fff; }
.dev.theme-dark.open { --device-face-fg: #252525; }
.dev.lock-locked {
--device-face-bg: light-dark(#000, #252525);
--device-face-fg: #fff;
--device-shell-stroke: light-dark(#000, #252525);
}
.dev.theme-light.lock-locked {
--device-face-bg: #000;
--device-shell-stroke: #000;
--device-face-bg: #66D17A;
--device-face-fg: light-dark(#fff, #252525);
--device-shell-stroke: #66D17A;
}
.dev.theme-light.lock-locked { --device-face-fg: #fff; }
.dev.theme-dark.lock-locked {
--device-face-bg: #252525;
--device-shell-stroke: #252525;
--device-face-fg: #252525;
--device-shell-stroke-ratio: .025;
}
.dev.lock-unlocked {
--device-face-bg: #F0A00C;
--device-face-bg: #F0410C;
--device-face-fg: light-dark(#fff, #252525);
--device-shell-stroke: #F0A00C;
--device-shell-stroke: #F0410C;
}
.dev.theme-light.lock-unlocked { --device-face-fg: #fff; }
.dev.theme-dark.lock-unlocked {
+35 -2
View File
@@ -34,12 +34,45 @@ test('issue 213 projects opening locks through the compact package layers', () =
assert.match(styles, /--oplock-size:\s*calc\(var\(--icon-size,\s*2\.5cqw\)\s*\*\s*0\.62\)/);
assert.match(styles, /--oplock-core-size:\s*calc\(var\(--oplock-size\)\s*\/\s*1\.26875\)/);
assert.match(styles, /\.oplock ha-icon\s*\{[\s\S]*--mdc-icon-size:[^;]*\*\s*0\.55/);
assert.match(styles, /\.oplock\.unlocked\s*\{[\s\S]*--oplock-core-bg:\s*#F0A00C/);
assert.doesNotMatch(styles, /\.oplock\.locked\s*\{[^}]*#66d17a/i);
assert.match(card, /class="oplock \$\{deviceThemeClass\(this\._renderPlanHass\)\}/);
assert.match(card, /class="oplock-shell"[\s\S]*class="oplock-core"/);
});
test('issue 219 gives locks red-open green-closed and themes every colored glyph', () => {
const styles = source('styles.ts');
const card = source('houseplan-card.ts');
for (const [selector, background] of [
['oplock\\.locked', '#66D17A'],
['oplock\\.unlocked', '#F0410C'],
['dev\\.lock-locked', '#66D17A'],
['dev\\.lock-unlocked', '#F0410C'],
]) {
const block = styles.match(new RegExp(`\\.${selector}\\s*\\{([\\s\\S]*?)\\}`))?.[1] || '';
assert.match(block, new RegExp(`(?:core|face)-bg:\\s*${background}`));
assert.match(block, new RegExp(`shell-stroke:\\s*${background}`));
assert.match(block, /(?:core|face)-fg:\s*light-dark\(#fff, #252525\)/);
}
const open = styles.match(/\.dev\.open\s*\{([\s\S]*?)\}/)?.[1] || '';
assert.match(open, /--device-face-bg:\s*var\(--hp-open\)/);
assert.match(open, /--device-face-fg:\s*light-dark\(#fff, #252525\)/);
assert.doesNotMatch(open, /#4a2800/i);
for (const state of ['on', 'open', 'lock-locked', 'lock-unlocked']) {
assert.match(styles, new RegExp(`\\.dev\\.theme-light\\.${state.replace('-', '\\-')}[^}]*#fff`));
assert.match(styles, new RegExp(`\\.dev\\.theme-dark\\.${state.replace('-', '\\-')}[\\s\\S]*?--device-face-fg:\\s*#252525`));
}
for (const state of ['locked', 'unlocked']) {
assert.match(styles, new RegExp(`\\.oplock\\.theme-light\\.${state}[^}]*#fff`));
assert.match(styles, new RegExp(`\\.oplock\\.theme-dark\\.${state}[\\s\\S]*?--oplock-core-fg:\\s*#252525`));
}
assert.match(card, /locked \? 'mdi:lock' : known \? 'mdi:lock-open-variant'/);
for (const name of ['houseplan-card.ts', 'hp-device-preview.ts', 'space-card.ts']) {
assert.match(source(name), /cardStyles/, `${name} must keep the shared state palette`);
}
});
test('issue 212 Text value uses a stadium radius based on height', () => {
const styles = source('styles.ts');
const rule = styles.match(/\.dev\.valonly \.device-core\s*\{([\s\S]*?)\}/)?.[1] || '';
+1
View File
@@ -321,6 +321,7 @@ test('device icon state-table goldens cover both themes and design facets', () =
assert.deepEqual(scenario.deviceClassOverrides['golden-presence'], ['virtual']);
assert.deepEqual(scenario.deviceClassOverrides['golden-light-two'], ['lock-locked']);
assert.deepEqual(scenario.deviceClassOverrides['golden-light-three'], ['lock-unlocked']);
assert.deepEqual(scenario.deviceClassOverrides['golden-right-linkquality'], ['open']);
const fixture = prepareGoldenFixture(scenario);
assert.equal(fixture.states['light.golden_light_one'].attributes.lqi, 40);
assert.equal(fixture.states['light.golden_light_two'].attributes.lqi, 41);