custom_components/bahmcloud_store/panel/panel.js aktualisiert

This commit is contained in:
2026-01-15 09:46:41 +00:00
parent bae4d0b84f
commit 77b4522e3c

View File

@@ -5,9 +5,7 @@ class BahmcloudStorePanel extends HTMLElement {
this._hass = null;
// Views: store | manage | about | detail
this._view = "store";
this._data = null;
this._loading = true;
this._error = null;
@@ -15,11 +13,9 @@ class BahmcloudStorePanel extends HTMLElement {
this._customAddUrl = "";
this._customAddName = "";
// Store filtering
this._search = "";
this._category = "all";
// Detail view state
this._detailRepoId = null;
this._detailRepo = null;
this._readmeLoading = false;
@@ -81,7 +77,6 @@ class BahmcloudStorePanel extends HTMLElement {
}
_goBack() {
// If we're in detail, go back to store list first.
if (this._view === "detail") {
this._view = "store";
this._detailRepoId = null;
@@ -244,10 +239,7 @@ class BahmcloudStorePanel extends HTMLElement {
border-color:var(--bcs-accent);
background: color-mix(in srgb, var(--bcs-accent) 16%, var(--card-background-color));
}
button:disabled{
opacity: 0.55;
cursor: not-allowed;
}
button:disabled{ opacity: 0.55; cursor: not-allowed; }
.card{
border:1px solid var(--divider-color);
@@ -277,14 +269,8 @@ class BahmcloudStorePanel extends HTMLElement {
.error{ color:#b00020; white-space:pre-wrap; margin-top:10px; }
.grid2{
display:grid;
grid-template-columns: 1fr;
gap: 12px;
}
@media (min-width: 900px){
.grid2{ grid-template-columns: 1.2fr 0.8fr; }
}
.grid2{ display:grid; grid-template-columns: 1fr; gap: 12px; }
@media (min-width: 900px){ .grid2{ grid-template-columns: 1.2fr 0.8fr; } }
.filters{
display:flex;
@@ -309,7 +295,6 @@ class BahmcloudStorePanel extends HTMLElement {
a{ color:var(--bcs-accent); text-decoration:none; }
a:hover{ text-decoration:underline; }
/* FABs (detail view) */
.fabs{
position: fixed;
right: 18px;
@@ -337,12 +322,8 @@ class BahmcloudStorePanel extends HTMLElement {
border-color: var(--bcs-accent);
background: color-mix(in srgb, var(--bcs-accent) 18%, var(--card-background-color));
}
.fab[disabled]{
opacity: .55;
cursor: not-allowed;
}
.fab[disabled]{ opacity: .55; cursor: not-allowed; }
/* README fallback pre */
pre.readme{
white-space: pre-wrap;
word-break: break-word;
@@ -351,6 +332,9 @@ class BahmcloudStorePanel extends HTMLElement {
font-size: 12.5px;
line-height: 1.5;
}
details{ margin-top: 10px; }
summary{ cursor:pointer; color: var(--bcs-accent); font-weight: 800; }
</style>
<div class="mobilebar">
@@ -392,8 +376,7 @@ class BahmcloudStorePanel extends HTMLElement {
});
}
// ✅ CRITICAL FIX: prevent HA global shortcuts (Assist/Command Palette) from
// stealing keystrokes while typing in inputs inside this panel.
// Prevent HA global shortcuts while typing inside panel inputs
const stopIfFormField = (e) => {
const t = e.composedPath ? e.composedPath()[0] : e.target;
if (!t) return;
@@ -410,14 +393,47 @@ class BahmcloudStorePanel extends HTMLElement {
}
};
// Use capture phase so we intercept before HA handlers.
root.addEventListener("keydown", stopIfFormField, true);
root.addEventListener("keyup", stopIfFormField, true);
root.addEventListener("keypress", stopIfFormField, true);
}
_captureFocusState() {
const root = this.shadowRoot;
const ae = root.activeElement;
if (!ae || !ae.id) return null;
const supported = new Set(["searchInput", "categorySelect", "addUrl", "addName"]);
if (!supported.has(ae.id)) return null;
const state = {
id: ae.id,
value: ae.value,
selectionStart: typeof ae.selectionStart === "number" ? ae.selectionStart : null,
selectionEnd: typeof ae.selectionEnd === "number" ? ae.selectionEnd : null,
};
return state;
}
_restoreFocusState(state) {
if (!state) return;
const root = this.shadowRoot;
const el = root.getElementById(state.id);
if (!el) return;
try {
el.focus({ preventScroll: true });
if (state.selectionStart !== null && state.selectionEnd !== null && typeof el.setSelectionRange === "function") {
el.setSelectionRange(state.selectionStart, state.selectionEnd);
}
} catch (_) {}
}
_update() {
const root = this.shadowRoot;
const focusState = this._captureFocusState();
const content = root.getElementById("content");
const err = root.getElementById("error");
const subtitle = root.getElementById("subtitle");
@@ -437,34 +453,39 @@ class BahmcloudStorePanel extends HTMLElement {
if (this._loading) {
content.innerHTML = `<div class="card">Loading…</div>`;
this._restoreFocusState(focusState);
return;
}
if (!this._data) {
content.innerHTML = `<div class="card">No data.</div>`;
this._restoreFocusState(focusState);
return;
}
if (this._view === "store") {
content.innerHTML = this._renderStore();
this._wireStore();
this._restoreFocusState(focusState);
return;
}
if (this._view === "manage") {
content.innerHTML = this._renderManage();
this._wireManage();
this._restoreFocusState(focusState);
return;
}
if (this._view === "about") {
content.innerHTML = this._renderAbout();
this._restoreFocusState(focusState);
return;
}
// detail
content.innerHTML = this._renderDetail();
this._wireDetail();
this._restoreFocusState(focusState);
}
_renderStore() {
@@ -488,32 +509,34 @@ class BahmcloudStorePanel extends HTMLElement {
...categories.map((c) => `<option value="${this._esc(c)}"${this._category === c ? " selected" : ""}>${this._esc(c)}</option>`),
].join("");
const rows = filtered
.map((r) => {
const badge = r.source === "custom"
? `<span class="badge custom">Custom</span>`
: `<span class="badge">Index</span>`;
const rows = filtered.map((r) => {
const badge = r.source === "custom"
? `<span class="badge custom">Custom</span>`
: `<span class="badge">Index</span>`;
const desc = r.description || r.meta_description || r.provider_description || "No description available.";
const creator = r.owner ? `Creator: ${r.owner}` : "Creator: -";
const cat = r.category ? `Category: ${r.category}` : null;
const metaSrc = r.meta_source ? `Meta: ${r.meta_source}` : null;
const lineBits = [creator, cat, metaSrc].filter(Boolean);
const desc = r.description || "No description available.";
return `
<div class="card clickable" data-repo="${this._esc(r.id)}">
<div class="row">
<div>
<div><strong>${this._esc(r.name)}</strong></div>
<div class="muted">${this._esc(desc)}</div>
<div class="muted small">${this._esc(lineBits.join(" · "))}</div>
</div>
${badge}
const creator = r.owner ? `Creator: ${r.owner}` : "Creator: -";
const cat = r.category ? `Category: ${r.category}` : null;
const metaSrc = r.meta_source ? `Meta: ${r.meta_source}` : null;
const latest = r.latest_version ? `Latest: ${r.latest_version}` : "Latest: unknown";
const lineBits = [creator, latest, cat, metaSrc].filter(Boolean);
return `
<div class="card clickable" data-repo="${this._esc(r.id)}">
<div class="row">
<div>
<div><strong>${this._esc(r.name)}</strong></div>
<div class="muted">${this._esc(desc)}</div>
<div class="muted small">${this._esc(lineBits.join(" · "))}</div>
</div>
${badge}
</div>
`;
})
.join("");
</div>
`;
}).join("");
return `
<div class="filters">
@@ -563,10 +586,13 @@ class BahmcloudStorePanel extends HTMLElement {
? `<span class="badge custom">Custom</span>`
: `<span class="badge">Index</span>`;
const desc = r.description || r.meta_description || r.provider_description || "No description available.";
const desc = r.description || "No description available.";
const latest = r.latest_version ? `Latest: ${r.latest_version}` : "Latest: unknown";
const infoBits = [
r.owner ? `Creator: ${r.owner}` : "Creator: -",
latest,
r.provider ? `Provider: ${r.provider}` : null,
r.category ? `Category: ${r.category}` : null,
r.meta_author ? `Author: ${r.meta_author}` : null,
@@ -580,8 +606,14 @@ class BahmcloudStorePanel extends HTMLElement {
? `
<div class="card">
<div><strong>README</strong></div>
<div class="muted small" style="margin-top:6px;">Rendered Markdown (fallback to raw text if needed).</div>
<div id="readmeContainer" style="margin-top:12px;"></div>
<details>
<summary>Show raw Markdown</summary>
<div style="margin-top:10px;">
<pre class="readme">${this._esc(this._readmeText)}</pre>
</div>
</details>
</div>
`
: `
@@ -628,9 +660,6 @@ class BahmcloudStorePanel extends HTMLElement {
}
_wireDetail() {
// Render README into container:
// 1) Try ha-markdown (if available)
// 2) Fallback: raw markdown in <pre>
const root = this.shadowRoot;
const container = root.getElementById("readmeContainer");
if (!container) return;
@@ -639,19 +668,22 @@ class BahmcloudStorePanel extends HTMLElement {
if (!this._readmeText) return;
// Try HA markdown element (best effort)
try {
const el = document.createElement("ha-markdown");
// some HA builds need hass set before content
try { el.hass = this._hass; } catch (_) {}
el.content = this._readmeText;
container.appendChild(el);
return;
} catch (_) {
// fall through
// Only attempt ha-markdown if the custom element exists
const hasHaMarkdown = typeof customElements !== "undefined" && !!customElements.get("ha-markdown");
if (hasHaMarkdown) {
try {
const el = document.createElement("ha-markdown");
try { el.hass = this._hass; } catch (_) {}
el.content = this._readmeText;
container.appendChild(el);
return;
} catch (_) {
// fall through to raw fallback (details already exists)
}
}
// Fallback: raw text
// If ha-markdown is missing, show a minimal rendered area:
const pre = document.createElement("pre");
pre.className = "readme";
pre.textContent = this._readmeText;
@@ -697,23 +729,21 @@ class BahmcloudStorePanel extends HTMLElement {
const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
const custom = repos.filter((r) => r.source === "custom");
const list = custom
.map((r) => {
return `
<div class="card">
<div class="row">
<div>
<div><strong>${this._esc(r.name)}</strong></div>
<div class="muted">${this._esc(r.url)}</div>
</div>
<div>
<button class="primary" data-remove="${this._esc(r.id)}">Remove</button>
</div>
const list = custom.map((r) => {
return `
<div class="card">
<div class="row">
<div>
<div><strong>${this._esc(r.name)}</strong></div>
<div class="muted">${this._esc(r.url)}</div>
</div>
<div>
<button class="primary" data-remove="${this._esc(r.id)}">Remove</button>
</div>
</div>
`;
})
.join("");
</div>
`;
}).join("");
return `
<div class="card">