diff --git a/custom_components/bahmcloud_store/panel/panel.js b/custom_components/bahmcloud_store/panel/panel.js
new file mode 100644
index 0000000..092e27b
--- /dev/null
+++ b/custom_components/bahmcloud_store/panel/panel.js
@@ -0,0 +1,602 @@
+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._readmeHtml = null;
+ this._readmeError = null;
+ }
+
+ 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 {
+ this.dispatchEvent(new Event("hass-toggle-menu", { bubbles: true, composed: true }));
+ 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._readmeHtml = null;
+ this._readmeError = null;
+ this._update();
+ return;
+ }
+ try {
+ history.back();
+ } catch (_) {
+ window.location.href = "/";
+ }
+ }
+
+ _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._readmeHtml = 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;
+ this._readmeHtml = typeof resp.html === "string" && resp.html.trim() ? resp.html : null;
+ } else {
+ this._readmeText = null;
+ this._readmeHtml = null;
+ this._readmeError = resp?.message || "README not found.";
+ }
+ } catch (e) {
+ this._readmeText = null;
+ this._readmeHtml = 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();
+ if (tag === "input" || tag === "textarea" || tag === "select" || t.isContentEditable) {
+ 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"]);
+ 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 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}` : "";
+
+ 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 = `Manage view is unchanged in this patch.
`;
+ 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 = Array.from(new Set(repos.map(r => (r.category || "").toLowerCase()).filter(Boolean))).sort();
+
+ 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 metaSrc = r.meta_source ? `Meta: ${r.meta_source}` : null;
+
+ const lineBits = [creator, latest, 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.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
+
Buttons will be enabled in a later version.
+
+
+
+ `;
+ }
+
+ _wireDetail() {
+ const root = this.shadowRoot;
+ const pretty = root.getElementById("readmePretty");
+ if (!pretty) return;
+
+ if (this._readmeHtml) {
+ pretty.innerHTML = this._readmeHtml;
+ } else if (this._readmeText) {
+ // If backend could not render HTML, show a friendly fallback text
+ pretty.innerHTML = `Markdown rendering is not available. Please use "Show raw Markdown".
`;
+ }
+ }
+
+ _renderAbout() {
+ return `
+
+
Settings / About
+
BCS version: ${this._esc(this._data.version || "-")}
+
+ `;
+ }
+
+ _esc(s) {
+ return String(s ?? "")
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+ }
+}
+
+customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);