diff --git a/custom_components/bahmcloud_store/panel/panel.js b/custom_components/bahmcloud_store/panel/panel.js
index f8c1b91..b9e5e07 100644
--- a/custom_components/bahmcloud_store/panel/panel.js
+++ b/custom_components/bahmcloud_store/panel/panel.js
@@ -15,6 +15,7 @@ class BahmcloudStorePanel extends HTMLElement {
this._search = "";
this._category = "all";
+ this._provider = "all"; // NEW: provider filter (all|github|gitea|gitlab|other|custom)
this._detailRepoId = null;
this._detailRepo = null;
@@ -268,12 +269,20 @@ class BahmcloudStorePanel extends HTMLElement {
.grid2{ display:grid; grid-template-columns: 1fr; gap: 12px; }
@media (min-width: 900px){ .grid2{ grid-template-columns: 1.2fr 0.8fr; } }
- .filters{
- display:flex;
- gap: 10px;
- flex-wrap: wrap;
- align-items: center;
- margin-bottom: 12px;
+ .filters{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-bottom:12px; }
+ .chips{ display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px; }
+
+ .chip{
+ display:inline-flex; align-items:center; gap:8px;
+ padding:6px 10px; border-radius:999px;
+ border:1px solid var(--divider-color);
+ background:var(--card-background-color);
+ cursor:pointer; user-select:none; font-weight:800; font-size:12px;
+ }
+ .chip strong{ font-size:12px; }
+ .chip.active{
+ border-color:var(--bcs-accent);
+ box-shadow:0 0 0 2px color-mix(in srgb, var(--bcs-accent) 18%, transparent);
}
input, select{
@@ -494,6 +503,9 @@ class BahmcloudStorePanel extends HTMLElement {
const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
const categories = this._computeCategories(repos);
+ // Provider stats (for overview)
+ const stats = this._computeProviderStats(repos);
+
const filtered = repos
.filter((r) => {
const q = (this._search || "").trim().toLowerCase();
@@ -504,6 +516,12 @@ class BahmcloudStorePanel extends HTMLElement {
.filter((r) => {
if (this._category === "all") return true;
return (r.category || "").toLowerCase() === this._category;
+ })
+ .filter((r) => {
+ if (this._provider === "all") return true;
+ if (this._provider === "custom") return r.source === "custom";
+ const pv = (r.provider || "other").toLowerCase();
+ return pv === this._provider;
});
const options = [
@@ -516,6 +534,8 @@ class BahmcloudStorePanel extends HTMLElement {
),
].join("");
+ const providerChips = this._renderProviderChips(stats);
+
const rows = filtered
.map((r) => {
const badge =
@@ -547,19 +567,70 @@ class BahmcloudStorePanel extends HTMLElement {
.join("");
return `
+
+
Providers
+
${providerChips}
+
+
- ${rows || `No repositories configured.
`}
+ ${rows || `No repositories match this filter.
`}
`;
}
+ _renderProviderChips(stats) {
+ const order = ["all", "github", "gitea", "gitlab", "other", "custom"];
+ const labels = {
+ all: "All",
+ github: "GitHub",
+ gitea: "Gitea",
+ gitlab: "GitLab",
+ other: "Other",
+ custom: "Custom",
+ };
+ const values = {
+ all: stats.total,
+ github: stats.github,
+ gitea: stats.gitea,
+ gitlab: stats.gitlab,
+ other: stats.other,
+ custom: stats.custom,
+ };
+
+ return order
+ .map((key) => {
+ const count = values[key] ?? 0;
+ const active = this._provider === key ? " active" : "";
+ return `${labels[key]} ${count}
`;
+ })
+ .join("");
+ }
+
+ _computeProviderStats(repos) {
+ const s = { total: 0, github: 0, gitea: 0, gitlab: 0, other: 0, custom: 0 };
+ if (!Array.isArray(repos)) return s;
+
+ for (const r of repos) {
+ s.total += 1;
+ if (r.source === "custom") s.custom += 1;
+
+ const p = (r.provider || "other").toLowerCase();
+ if (p === "github") s.github += 1;
+ else if (p === "gitea") s.gitea += 1;
+ else if (p === "gitlab") s.gitlab += 1;
+ else s.other += 1;
+ }
+ return s;
+ }
+
_wireStore() {
const root = this.shadowRoot;
const search = root.getElementById("searchInput");
const cat = root.getElementById("categorySelect");
+ const chips = root.getElementById("providerChips");
if (search) {
search.addEventListener("input", (e) => {
@@ -575,6 +646,17 @@ class BahmcloudStorePanel extends HTMLElement {
});
}
+ if (chips) {
+ for (const c of chips.querySelectorAll("[data-prov]")) {
+ c.addEventListener("click", () => {
+ const key = c.getAttribute("data-prov");
+ if (!key) return;
+ this._provider = key; // set filter
+ this._update();
+ });
+ }
+ }
+
for (const card of root.querySelectorAll("[data-repo]")) {
card.addEventListener("click", () => {
const id = card.getAttribute("data-repo");
@@ -666,18 +748,43 @@ class BahmcloudStorePanel extends HTMLElement {
`;
}
+ _wireDetail() {
+ const root = this.shadowRoot;
+ const mount = root.getElementById("readmePretty");
+ if (!mount) return;
+
+ if (this._readmeText) {
+ const html = this._mdToHtml(this._readmeText);
+
+ if (html) {
+ mount.innerHTML = html;
+ this._postprocessRenderedMarkdown(mount);
+ return;
+ }
+
+ if (this._readmeHtml) {
+ mount.innerHTML = this._readmeHtml;
+ this._postprocessRenderedMarkdown(mount);
+ return;
+ }
+
+ mount.innerHTML = `Markdown renderer not available on this client. Use “Show raw Markdown”.
`;
+ return;
+ }
+
+ mount.innerHTML = "";
+ }
+
_mdToHtml(markdown) {
const md = String(markdown || "");
if (!md.trim()) return "";
- // Prefer HA's frontend libs if present
const markedObj = window.marked;
const domPurify = window.DOMPurify;
let html = "";
try {
- // marked can be a function or an object with parse()
if (markedObj && typeof markedObj.parse === "function") {
html = markedObj.parse(md, { breaks: true, gfm: true });
} else if (typeof markedObj === "function") {
@@ -700,8 +807,6 @@ class BahmcloudStorePanel extends HTMLElement {
_postprocessRenderedMarkdown(container) {
if (!container) return;
-
- // Make links open in new tab
try {
const links = container.querySelectorAll("a[href]");
links.forEach((a) => {
@@ -711,36 +816,6 @@ class BahmcloudStorePanel extends HTMLElement {
} catch (_) {}
}
- _wireDetail() {
- const root = this.shadowRoot;
- const mount = root.getElementById("readmePretty");
- if (!mount) return;
-
- if (this._readmeText) {
- const html = this._mdToHtml(this._readmeText);
-
- if (html) {
- mount.innerHTML = html;
- this._postprocessRenderedMarkdown(mount);
- return;
- }
-
- // Fallback: if backend provided sanitized HTML, use it
- if (this._readmeHtml) {
- mount.innerHTML = this._readmeHtml;
- this._postprocessRenderedMarkdown(mount);
- return;
- }
-
- // Last resort
- mount.innerHTML = `Markdown renderer not available on this client. Use “Show raw Markdown”.
`;
- return;
- }
-
- // No readme text loaded
- mount.innerHTML = "";
- }
-
_renderFabs() {
const r = this._detailRepo;
if (!r) return "";
@@ -879,4 +954,4 @@ class BahmcloudStorePanel extends HTMLElement {
}
}
-customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);
+customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);
\ No newline at end of file