fix(card): static «Show as LED strip» in the device dialog; demo icons (#780)

The full Validate on a6c4001c found two things:
- smoke_help_affordance: the device dialog shifted when the LED tool chunk
  arrived after it opened (the representation section appeared late). A
  device without a strip now gets the static section at once (label in the
  base dictionary); the press loads the tool, leaves the dialog through its
  own guard and starts drawing for the same marker. A strip's device still
  loads the tool with the dialog. The dialog alone no longer loads anything.
- smoke_lazy_admin_locale counted nine namespace chunks (stale since the
  `tools` namespace); it now reads NAMESPACE_LOCALE_CHUNKS and accepts the
  editor's own `tools-de`.
- demo/srv/assets/icons.js regenerated: the demo/golden ha-icon stub lacked
  mdi:led-strip-variant and mdi:link-variant(-off).

Issue: #780
User-Visible: no
This commit is contained in:
Codex
2026-10-02 14:10:28 +03:00
parent a6c4001caa
commit 63e751ad19
9 changed files with 57 additions and 9 deletions
+5 -2
View File
@@ -9,10 +9,13 @@
// отказ — английский в строках пространства, один тост, без вечной загрузки.
import { readFileSync } from 'node:fs';
import { launchColdView, checkAll, finish } from './serve.mjs';
import { NAMESPACE_LOCALE_CHUNKS } from '../scripts/bundle-manifest.mjs';
const manifest = JSON.parse(readFileSync('dist/houseplan-assets.json', 'utf8'));
const NAMESPACE_CHUNKS = (manifest.lazyNamespaceLocaleFiles || []).map((path) => path.split('/').at(-1));
if (NAMESPACE_CHUNKS.length !== 9) throw new Error('bundle manifest has no nine namespace locale chunks');
// Every namespace × ru/de/fr is its own chunk (#627; `tools` #780 stage 3, `led` #780).
if (NAMESPACE_CHUNKS.length !== NAMESPACE_LOCALE_CHUNKS.length)
throw new Error(`bundle manifest has ${NAMESPACE_CHUNKS.length} namespace locale chunks, expected ${NAMESPACE_LOCALE_CHUNKS.length}`);
const chunkName = (namespace, language) => {
const name = NAMESPACE_CHUNKS.find((candidate) => candidate.startsWith(`${namespace}-${language}-`));
if (!name) throw new Error(`namespace chunk ${namespace}-${language} is absent from the manifest`);
@@ -178,7 +181,7 @@ const out = {};
out.settingsRequestsEachGermanNamespaceOnce = namespaces
.every((namespace) => count(requested, chunkName(namespace, 'de')) === 1);
out.settingsRequestsNoOtherLanguage = namespaceRequests(requested)
.every((name) => /^(settings|support|topology)-de-/.test(name));
.every((name) => /^(settings|support|topology|tools)-de-/.test(name));
await browser.close();
}
+23
View File
@@ -9,6 +9,7 @@
* and computes no field with `light_pools: false`.
*/
import { launch, check, finish } from './serve.mjs';
import { installHpTestOnPage } from './helpers/hp-test.mjs';
const { page, browser } = await launch({ width: 1000, height: 820 }, 1);
const ledRequests = [];
@@ -132,4 +133,26 @@ const staticCards = await evaluate(async () => {
check('static card without light_pools: passive stripe, no field', JSON.stringify(staticCards.plain), JSON.stringify({ stripe: true, field: false, hit: false }));
check('static card with light_pools: the field', JSON.stringify(staticCards.pools), JSON.stringify({ stripe: true, field: true, hit: false }));
// The device dialog offers «Show as LED strip» before the tool is loaded:
// a static section (no dialog shift), the press loads the tool and starts
// drawing for this same marker after the dialog's own close path.
// A fresh page without strips: entering the Devices editor alone loads nothing.
await page.reload();
await page.waitForFunction(() => window.__card?._loadOk);
await installHpTestOnPage(page);
ledRequests.length = 0;
const before = 0;
await evaluate(async () => { await window.__hpTest.setMode('devices'); await window.__hpTest.openMarkerDialog('d_tv'); });
await page.waitForTimeout(200);
check('static «Show as LED strip» without the tool chunk', await evaluate(() =>
!!window.__card.shadowRoot.querySelector('[data-led-representation="icon"] [data-led-action="show-strip"]')),
true);
check('the dialog alone loads no LED tool', ledRequests.filter((name) => name.startsWith('led-strip-editor-')).length, before);
await page.click('[data-led-representation] [data-led-action="show-strip"]');
await page.waitForTimeout(600);
check('the press loads the tool and draws for the same marker', await evaluate(() => {
const led = window.__card._ledEditor;
return !!led?.tool && led.chain?.convert === 'd_tv' && !window.__card._markerDialog;
}));
await finish(browser);
File diff suppressed because one or more lines are too long
+1
View File
@@ -609,6 +609,7 @@
"title.add_device": "Gerät zum Plan hinzufügen",
"devbar.add": "Hinzufügen",
"devbar.led": "LED-Streifen",
"devbar.led_show": "Als LED-Streifen zeigen",
"devbar.rules": "Symbolregeln",
"device_inbox.button": "Geräte",
"device_inbox.title": "Geräte auf dem Plan",
+1
View File
@@ -609,6 +609,7 @@
"title.add_device": "Add a device to the plan",
"devbar.add": "Add",
"devbar.led": "LED strip",
"devbar.led_show": "Show as LED strip",
"devbar.rules": "Icon rules",
"device_inbox.button": "Devices",
"device_inbox.title": "Devices on the plan",
+1
View File
@@ -609,6 +609,7 @@
"title.add_device": "Ajouter un appareil au plan",
"devbar.add": "Ajouter",
"devbar.led": "Ruban LED",
"devbar.led_show": "Afficher en ruban LED",
"devbar.rules": "Règles d’icônes",
"device_inbox.button": "Appareils",
"device_inbox.title": "Appareils sur le plan",
+1
View File
@@ -609,6 +609,7 @@
"title.add_device": "Добавить устройство на план",
"devbar.add": "Добавить",
"devbar.led": "LED-лента",
"devbar.led_show": "Показывать LED-лентой",
"devbar.rules": "Правила иконок",
"device_inbox.button": "Устройства",
"device_inbox.title": "Устройства на плане",
+19 -6
View File
@@ -41,15 +41,28 @@ export function ledButton(card: LedCardPort): TemplateResult {
/**
* The device dialog's "icon ↔ LED strip" section — in the Devices editor
* only (ТЗ §5). A strip's dialog elsewhere still loads the tool: its save
* renames the link on a rebinding.
* only (ТЗ §5). A device without a strip gets the static «Show as LED strip»
* at once (the tool loads on the press, the dialog does not shift); a strip's
* device loads the tool, also outside Devices: its save renames the link on a
* rebinding.
*/
export function ledSection(card: LedCardPort, devId: string): TemplateResult | typeof nothing {
const devices = card._mode === 'devices';
if (!devices && !card._serverCfg?.spaces.some((space: { led_strips?: Array<{ marker: string | null }> }) =>
space.led_strips?.some((strip) => strip.marker === devId))) return nothing;
const led = ledEditorFor(card, 'dialog');
return devices ? led?.markerSection(devId) ?? nothing : nothing;
const owned = !!card._serverCfg?.spaces.some((space: { led_strips?: Array<{ marker: string | null }> }) =>
space.led_strips?.some((strip) => strip.marker === devId));
if (!devices) {
if (owned) ledEditorFor(card, 'dialog');
return nothing;
}
if (card._ledEditor) return card._ledEditor.markerSection(devId);
if (owned) { ledEditorFor(card, 'dialog'); return nothing; }
return html`<div class="hpf-group led-representation" data-led-representation="icon">
<div class="row" style="flex-wrap:wrap;gap:6px"><button class="btn ghost" type="button" data-led-action="show-strip"
@click=${() => {
const entry = `tool${Date.now()}`, go = () => ledEditorFor(card, entry)?.convertFromDialog(devId);
if (ledEditorModule(entry, go)) go();
}}><ha-icon icon="mdi:led-strip-variant"></ha-icon>${card._t('devbar.led_show')}</button></div>
</div>`;
}
const hasStrips = (config: ServerConfig | null | undefined) => !!config?.spaces.some(
+5
View File
@@ -808,6 +808,11 @@ export class LedStripEditor {
return !this.host._markerDialog;
}
/** «Show as LED strip» pressed before the tool was loaded: same guard, then draw. */
async convertFromDialog(devId: string): Promise<void> {
if (await this.leaveDialog()) this.open(devId);
}
private crossings = new WeakMap<object, { n: number; spaces: string[] }>();
/**