diff --git a/custom_components/bahmcloud_store/panel/panel.js b/custom_components/bahmcloud_store/panel/panel.js index 6b8c023..a065331 100644 --- a/custom_components/bahmcloud_store/panel/panel.js +++ b/custom_components/bahmcloud_store/panel/panel.js @@ -4,13 +4,27 @@ class BahmcloudStorePanel extends HTMLElement { this.attachShadow({ mode: "open" }); this._hass = null; - this._view = "store"; // store | manage | about + + // Views: store | manage | about | detail + this._view = "store"; + this._data = null; this._loading = true; this._error = null; this._customAddUrl = ""; this._customAddName = ""; + + // Store filtering + this._search = ""; + this._category = "all"; + + // Detail view state + this._detailRepoId = null; + this._detailRepo = null; + this._readmeLoading = false; + this._readmeText = null; + this._readmeError = null; } set hass(hass) { @@ -67,6 +81,17 @@ 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; + this._detailRepo = null; + this._readmeText = null; + this._readmeError = null; + this._update(); + return; + } + try { history.back(); } catch (_) { @@ -90,11 +115,7 @@ class BahmcloudStorePanel extends HTMLElement { this._update(); try { - await this._hass.callApi("post", "bcs", { - op: "add_custom_repo", - url, - name, - }); + await this._hass.callApi("post", "bcs", { op: "add_custom_repo", url, name }); this._customAddUrl = ""; this._customAddName = ""; await this._load(); @@ -120,6 +141,49 @@ class BahmcloudStorePanel extends HTMLElement { } } + _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") { + 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; @@ -156,13 +220,13 @@ class BahmcloudStorePanel extends HTMLElement { color:var(--primary-text-color); } - .tabs{ display:flex; gap:8px; flex-wrap:wrap; margin-bottom:6px; } + .tabs{ display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px; } .tab{ border:1px solid var(--divider-color); background:var(--card-background-color); color:var(--primary-text-color); padding:8px 12px; border-radius:999px; - cursor:pointer; font-weight:600; font-size:13px; + cursor:pointer; font-weight:700; font-size:13px; } .tab.active{ border-color:var(--bcs-accent); @@ -174,49 +238,122 @@ class BahmcloudStorePanel extends HTMLElement { border:1px solid var(--divider-color); background:var(--card-background-color); color:var(--primary-text-color); - cursor:pointer; font-weight:700; + cursor:pointer; font-weight:800; } button.primary{ 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; + } .card{ border:1px solid var(--divider-color); background:var(--card-background-color); border-radius:16px; padding:12px; margin:10px 0; } - .row{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; } + .card.clickable{ + cursor:pointer; + transition: transform 120ms ease, box-shadow 120ms ease; + } + .card.clickable:hover{ + transform: translateY(-1px); + box-shadow: 0 10px 30px rgba(0,0,0,0.08); + } + + .row{ display:flex; justify-content:space-between; gap:10px; align-items:flex-start; } .muted{ color:var(--secondary-text-color); font-size:13px; margin-top:4px; } .small{ font-size:12px; } .badge{ border:1px solid var(--divider-color); border-radius:999px; padding:2px 10px; - font-size:12px; font-weight:700; height:fit-content; + font-size:12px; font-weight:800; height:fit-content; } .badge.custom{ border-color:var(--bcs-accent); color:var(--bcs-accent); } .error{ color:#b00020; white-space:pre-wrap; margin-top:10px; } - .grid{ display:grid; grid-template-columns:1fr; gap:10px; } - .field{ display:grid; gap:6px; } + .grid2{ + display:grid; + grid-template-columns: 1fr; + gap: 12px; + } + @media (min-width: 900px){ + .grid2{ grid-template-columns: 1.2fr 0.8fr; } + } - input{ + .filters{ + display:flex; + gap: 10px; + flex-wrap: wrap; + align-items: center; + margin-bottom: 12px; + } + + input, select{ padding:10px 12px; border-radius:12px; border:1px solid var(--divider-color); background:var(--card-background-color); color:var(--primary-text-color); outline:none; } - input:focus{ + input:focus, select:focus{ border-color:var(--bcs-accent); box-shadow:0 0 0 2px color-mix(in srgb, var(--bcs-accent) 20%, transparent); } a{ color:var(--bcs-accent); text-decoration:none; } a:hover{ text-decoration:underline; } + + /* FABs (detail view) */ + .fabs{ + position: fixed; + right: 18px; + bottom: 18px; + display: grid; + gap: 10px; + z-index: 100; + } + .fab{ + width: 56px; + height: 56px; + border-radius: 18px; + border: 1px solid var(--divider-color); + background: var(--card-background-color); + box-shadow: 0 12px 30px rgba(0,0,0,0.14); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: 900; + cursor: pointer; + user-select: none; + } + .fab.primary{ + 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-label{ + position: fixed; + right: 84px; + bottom: 18px; + display: none; + } + + .mdwrap ha-markdown{ + display: block; + } + .mdwrap{ + padding-top: 6px; + }