diff --git a/custom_components/bahmcloud_store/panel/panel.js b/custom_components/bahmcloud_store/panel/panel.js
deleted file mode 100644
index 1c7cc29..0000000
--- a/custom_components/bahmcloud_store/panel/panel.js
+++ /dev/null
@@ -1,876 +0,0 @@
-class BahmcloudStorePanel extends HTMLElement {
- constructor() {
- super();
- this.attachShadow({ mode: "open" });
-
- this._hass = null;
-
- this._view = "store";
- this._data = null;
- this._loading = true;
- this._error = null;
-
- this._customAddUrl = "";
- this._customAddName = "";
-
- this._search = "";
- this._category = "all";
-
- this._detailRepoId = null;
- this._detailRepo = null;
- this._readmeLoading = false;
- this._readmeText = null;
- this._readmeError = null;
-
- this._mdMountToken = 0;
- }
-
- set hass(hass) {
- this._hass = hass;
- if (!this._rendered) {
- this._rendered = true;
- this._render();
- this._load();
- }
- }
-
- async _load() {
- if (!this._hass) return;
-
- this._loading = true;
- this._error = null;
- this._update();
-
- try {
- const data = await this._hass.callApi("get", "bcs");
- this._data = data;
- } catch (e) {
- this._error = e?.message ? String(e.message) : String(e);
- } finally {
- this._loading = false;
- this._update();
- }
- }
-
- _isDesktop() {
- return window.matchMedia && window.matchMedia("(min-width: 1024px)").matches;
- }
-
- _toggleMenu() {
- if (this._isDesktop()) return;
-
- try {
- const ev = new Event("hass-toggle-menu", { bubbles: true, composed: true });
- this.dispatchEvent(ev);
- return;
- } catch (_) {}
-
- try {
- const host = this.shadowRoot?.host;
- if (host) {
- const ev2 = new Event("hass-toggle-menu", { bubbles: true, composed: true });
- host.dispatchEvent(ev2);
- return;
- }
- } catch (_) {}
-
- this._error = "Unable to open the sidebar on this client. Use the back button.";
- this._update();
- }
-
- _goBack() {
- if (this._view === "detail") {
- this._view = "store";
- this._detailRepoId = null;
- this._detailRepo = null;
- this._readmeText = null;
- this._readmeError = null;
- this._update();
- return;
- }
-
- try {
- history.back();
- } catch (_) {
- window.location.href = "/";
- }
- }
-
- async _addCustomRepo() {
- if (!this._hass) return;
-
- const url = (this._customAddUrl || "").trim();
- const name = (this._customAddName || "").trim() || null;
-
- if (!url) {
- this._error = "Please enter a repository URL.";
- this._update();
- return;
- }
-
- this._error = null;
- this._update();
-
- try {
- await this._hass.callApi("post", "bcs", { op: "add_custom_repo", url, name });
- this._customAddUrl = "";
- this._customAddName = "";
- await this._load();
- this._view = "manage";
- this._update();
- } catch (e) {
- this._error = e?.message ? String(e.message) : String(e);
- this._update();
- }
- }
-
- async _removeCustomRepo(id) {
- if (!this._hass) return;
-
- try {
- await this._hass.callApi("delete", `bcs/custom_repo?id=${encodeURIComponent(id)}`);
- await this._load();
- this._view = "manage";
- this._update();
- } catch (e) {
- this._error = e?.message ? String(e.message) : String(e);
- this._update();
- }
- }
-
- _openRepoDetail(repoId) {
- const repos = Array.isArray(this._data?.repos) ? this._data.repos : [];
- const repo = repos.find((r) => r.id === repoId);
- if (!repo) return;
-
- this._view = "detail";
- this._detailRepoId = repoId;
- this._detailRepo = repo;
-
- this._readmeText = null;
- this._readmeError = null;
-
- this._update();
- this._loadReadme(repoId);
- }
-
- async _loadReadme(repoId) {
- if (!this._hass) return;
- this._readmeLoading = true;
- this._readmeError = null;
- this._update();
-
- try {
- const resp = await this._hass.callApi(
- "get",
- `bcs/readme?repo_id=${encodeURIComponent(repoId)}`
- );
-
- if (resp?.ok && typeof resp.readme === "string" && resp.readme.trim()) {
- this._readmeText = resp.readme;
- } else {
- this._readmeText = null;
- this._readmeError = resp?.message || "README not found.";
- }
- } catch (e) {
- this._readmeText = null;
- this._readmeError = e?.message ? String(e.message) : String(e);
- } finally {
- this._readmeLoading = false;
- this._update();
- }
- }
-
- _render() {
- const root = this.shadowRoot;
-
- root.innerHTML = `
-
-
-
-
-
-
←
-
-
Bahmcloud Store
-
BCS — loading…
-
-
-
-
-
-
-
-
-
-
Store
-
Manage repositories
-
Settings / About
-
-
-
-
-
-
-
- `;
-
- root.getElementById("refreshBtn").addEventListener("click", () => this._load());
- root.getElementById("menuBtn").addEventListener("click", () => this._toggleMenu());
- root.getElementById("backBtn").addEventListener("click", () => this._goBack());
-
- for (const tab of root.querySelectorAll(".tab")) {
- tab.addEventListener("click", () => {
- this._view = tab.getAttribute("data-view");
- this._update();
- });
- }
-
- // Prevent HA global shortcuts while typing inside panel inputs
- const stopIfFormField = (e) => {
- const t = e.composedPath ? e.composedPath()[0] : e.target;
- if (!t) return;
-
- const tag = (t.tagName || "").toLowerCase();
- const isEditable =
- tag === "input" ||
- tag === "textarea" ||
- tag === "select" ||
- t.isContentEditable;
-
- if (isEditable) e.stopPropagation();
- };
-
- 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;
-
- return {
- id: ae.id,
- value: ae.value,
- selectionStart: typeof ae.selectionStart === "number" ? ae.selectionStart : null,
- selectionEnd: typeof ae.selectionEnd === "number" ? ae.selectionEnd : null,
- };
- }
-
- _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");
- const fabs = root.getElementById("fabs");
-
- const v = this._data?.version ? String(this._data.version) : null;
- subtitle.textContent = v ? `BCS ${v}` : "BCS — loading…";
-
- for (const tab of root.querySelectorAll(".tab")) {
- tab.classList.toggle("active", tab.getAttribute("data-view") === this._view);
- }
-
- err.textContent = this._error ? `Error: ${this._error}` : "";
-
- fabs.innerHTML = this._view === "detail" ? this._renderFabs() : "";
- this._wireFabs();
-
- if (this._loading) {
- content.innerHTML = `Loading…
`;
- this._restoreFocusState(focusState);
- return;
- }
-
- if (!this._data) {
- content.innerHTML = `No data.
`;
- 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;
- }
-
- content.innerHTML = this._renderDetail();
- this._wireDetail();
- this._restoreFocusState(focusState);
- }
-
- _renderStore() {
- const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
- const categories = this._computeCategories(repos);
-
- const filtered = repos
- .filter((r) => {
- const q = (this._search || "").trim().toLowerCase();
- if (!q) return true;
- const hay = `${r.name || ""} ${r.description || ""} ${r.url || ""} ${r.owner || ""}`.toLowerCase();
- return hay.includes(q);
- })
- .filter((r) => {
- if (this._category === "all") return true;
- return (r.category || "").toLowerCase() === this._category;
- });
-
- const options = [
- ``,
- ...categories.map((c) => ``),
- ].join("");
-
- const rows = filtered.map((r) => {
- const badge = r.source === "custom"
- ? `Custom`
- : `Index`;
-
- const desc = r.description || "No description available.";
- const creator = r.owner ? `Creator: ${r.owner}` : "Creator: -";
- const latest = r.latest_version ? `Latest: ${r.latest_version}` : "Latest: unknown";
- const cat = r.category ? `Category: ${r.category}` : null;
- const metaSrc = r.meta_source ? `Meta: ${r.meta_source}` : null;
-
- const lineBits = [creator, latest, cat, metaSrc].filter(Boolean);
-
- return `
-
-
-
-
${this._esc(r.name)}
-
${this._esc(desc)}
-
${this._esc(lineBits.join(" · "))}
-
- ${badge}
-
-
- `;
- }).join("");
-
- return `
-
-
-
-
-
- ${rows || `No repositories configured.
`}
- `;
- }
-
- _wireStore() {
- const root = this.shadowRoot;
- const search = root.getElementById("searchInput");
- const cat = root.getElementById("categorySelect");
-
- if (search) {
- search.addEventListener("input", (e) => {
- this._search = e.target.value;
- this._update();
- });
- }
-
- if (cat) {
- cat.addEventListener("change", (e) => {
- this._category = String(e.target.value || "all");
- this._update();
- });
- }
-
- for (const card of root.querySelectorAll("[data-repo]")) {
- card.addEventListener("click", () => {
- const id = card.getAttribute("data-repo");
- if (id) this._openRepoDetail(id);
- });
- }
- }
-
- _renderDetail() {
- const r = this._detailRepo;
- if (!r) return `Repository not found.
`;
-
- const badge = r.source === "custom"
- ? `Custom`
- : `Index`;
-
- 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,
- r.meta_maintainer ? `Maintainer: ${r.meta_maintainer}` : null,
- r.meta_source ? `Meta: ${r.meta_source}` : null,
- ].filter(Boolean);
-
- const readmeBlock = this._readmeLoading
- ? `Loading README…
`
- : this._readmeText
- ? `
-
-
README
-
-
-
- Show raw Markdown
-
-
${this._esc(this._readmeText)}
-
-
-
- `
- : `
-
-
README
-
${this._esc(this._readmeError || "README not found.")}
-
- `;
-
- return `
-
-
-
-
-
-
${this._esc(r.name)}
-
${this._esc(desc)}
-
${this._esc(infoBits.join(" · "))}
-
-
- ${badge}
-
-
-
- ${readmeBlock}
-
-
-
-
-
Installation & Updates
-
- Installation & updates will be implemented via the official Home Assistant APIs.
- In v0.4.0, the buttons are UI-only (coming soon).
-
-
- Updates remain manual (like HACS).
-
-
-
-
- `;
- }
-
- async _mountPrettyMarkdown(container, markdown) {
- const token = ++this._mdMountToken;
- container.innerHTML = "";
-
- const stillValid = () =>
- token === this._mdMountToken &&
- this._view === "detail" &&
- typeof this._readmeText === "string" &&
- this._readmeText.trim().length > 0;
-
- // 1) BEST: Use Lovelace markdown card (hui-markdown-card)
- try {
- await customElements.whenDefined("hui-markdown-card");
- if (!stillValid()) return;
-
- const wrap = document.createElement("div");
- wrap.className = "mdwrap";
- container.appendChild(wrap);
-
- const card = document.createElement("hui-markdown-card");
- wrap.appendChild(card);
-
- // Lovelace cards use setConfig + hass
- if (typeof card.setConfig === "function") {
- card.setConfig({
- type: "markdown",
- content: markdown,
- });
- } else {
- // fallback property name (just in case)
- card.config = { type: "markdown", content: markdown };
- }
-
- // Provide hass after config
- try { card.hass = this._hass; } catch (_) {}
-
- // Nudge a render pass
- await new Promise((resolve) => requestAnimationFrame(resolve));
- if (!stillValid()) return;
- try { card.hass = this._hass; } catch (_) {}
-
- return;
- } catch (_) {
- // Continue to next fallback
- }
-
- // 2) Fallback: ha-markdown
- try {
- await customElements.whenDefined("ha-markdown");
- if (!stillValid()) return;
-
- const el = document.createElement("ha-markdown");
- container.appendChild(el);
- try { el.hass = this._hass; } catch (_) {}
- try { el.content = markdown; } catch (_) {
- try { el.markdown = markdown; } catch (_) {}
- }
- return;
- } catch (_) {}
-
- // 3) Last fallback: plain text
- const pre = document.createElement("pre");
- pre.className = "readme";
- pre.textContent = markdown;
- container.appendChild(pre);
- }
-
- _wireDetail() {
- const root = this.shadowRoot;
- const container = root.getElementById("readmeContainer");
- if (!container) return;
-
- container.innerHTML = "";
- if (!this._readmeText) return;
-
- this._mountPrettyMarkdown(container, this._readmeText);
- }
-
- _renderFabs() {
- const r = this._detailRepo;
- if (!r) return "";
-
- return `
-
- `;
- }
-
- _wireFabs() {
- const root = this.shadowRoot;
- const r = this._detailRepo;
- if (!r) return;
-
- const open = root.getElementById("fabOpen");
- const reload = root.getElementById("fabReload");
-
- if (open) {
- open.addEventListener("click", () => {
- window.open(r.url, "_blank", "noreferrer");
- });
- }
-
- if (reload) {
- reload.addEventListener("click", () => {
- if (this._detailRepoId) this._loadReadme(this._detailRepoId);
- });
- }
- }
-
- _renderManage() {
- const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
- const custom = repos.filter((r) => r.source === "custom");
-
- const list = custom.map((r) => {
- return `
-
-
-
-
${this._esc(r.name)}
-
${this._esc(r.url)}
-
-
-
-
-
-
- `;
- }).join("");
-
- return `
-
-
Manage repositories
-
Add public repositories from any git provider.
-
-
-
-
-
-
Display name (optional)
-
-
-
-
-
-
-
-
-
- ${list || `No custom repositories added yet.
`}
- `;
- }
-
- _wireManage() {
- const root = this.shadowRoot;
-
- const addUrl = root.getElementById("addUrl");
- const addName = root.getElementById("addName");
- const addBtn = root.getElementById("addBtn");
-
- if (addUrl) addUrl.addEventListener("input", (e) => (this._customAddUrl = e.target.value));
- if (addName) addName.addEventListener("input", (e) => (this._customAddName = e.target.value));
- if (addBtn) addBtn.addEventListener("click", () => this._addCustomRepo());
-
- for (const btn of root.querySelectorAll("[data-remove]")) {
- btn.addEventListener("click", () => {
- const id = btn.getAttribute("data-remove");
- if (id) this._removeCustomRepo(id);
- });
- }
- }
-
- _renderAbout() {
- return `
-
-
Settings / About
-
Language: English (v1). i18n will be added later.
-
Theme: follows Home Assistant light/dark automatically.
-
Accent: Bahmcloud Blue.
-
BCS version: ${this._esc(this._data.version || "-")}
-
- `;
- }
-
- _computeCategories(repos) {
- const set = new Set();
- for (const r of repos) {
- const c = (r.category || "").trim().toLowerCase();
- if (c) set.add(c);
- }
- return Array.from(set).sort();
- }
-
- _esc(s) {
- return String(s ?? "")
- .replaceAll("&", "&")
- .replaceAll("<", "<")
- .replaceAll(">", ">")
- .replaceAll('"', """)
- .replaceAll("'", "'");
- }
-}
-
-customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);