diff --git a/README.md b/README.md
index fbc183a..63ebd51 100644
--- a/README.md
+++ b/README.md
@@ -250,8 +250,9 @@ ecosystem/
| | `tags` | Массив строк для поиска |
| | `id` | Стабильный ключ (DOM, фокус из дерева) |
| | `number` | Отображаемый номер |
+| | `icon` | Опционально: свой путь к SVG (иначе — иконка из `type`) |
-- `type` — ключ из встроенного `defaultServiceMeta` или из `serviceMeta` в JSON
+- `type` — ключ из встроенного `defaultServiceMeta` или из `serviceMeta` в JSON; определяет иконку и подпись на карточке
- `id` элемента нужен, если нет `number` — иначе ключ генерируется автоматически
### Примеры данных
@@ -283,6 +284,8 @@ npx serve .
`ceph`, `cloudbeaver`, `config`, `gitea`, `grafana`, `ipmi`, `kubernetes`, `ldap`, `messages`, `opensearch`, `postgresql`, `prometheus`, `rabbitmq`, `redmine`, `rules`, `xpath`, `xmpp`
+Каждый тип имеет собственную SVG в `assets/` (например, `postgresql.svg`, `cloudbeaver.svg`). Иконка показывается на карточке каталога и в дереве навигации.
+
Переопределение или добавление типов в JSON:
```json
diff --git a/assets/cloudbeaver.svg b/assets/cloudbeaver.svg
new file mode 100644
index 0000000..d3b97fd
--- /dev/null
+++ b/assets/cloudbeaver.svg
@@ -0,0 +1 @@
+
diff --git a/assets/postgresql.svg b/assets/postgresql.svg
new file mode 100644
index 0000000..498a401
--- /dev/null
+++ b/assets/postgresql.svg
@@ -0,0 +1 @@
+
diff --git a/examples/shared/assets/cloudbeaver.svg b/examples/shared/assets/cloudbeaver.svg
new file mode 100644
index 0000000..d3b97fd
--- /dev/null
+++ b/examples/shared/assets/cloudbeaver.svg
@@ -0,0 +1 @@
+
diff --git a/examples/shared/assets/postgresql.svg b/examples/shared/assets/postgresql.svg
new file mode 100644
index 0000000..498a401
--- /dev/null
+++ b/examples/shared/assets/postgresql.svg
@@ -0,0 +1 @@
+
diff --git a/examples/shared/script.js b/examples/shared/script.js
index 411d0f3..f3f6295 100644
--- a/examples/shared/script.js
+++ b/examples/shared/script.js
@@ -33,7 +33,7 @@ let panelTitle = "Панель экосистемы";
const defaultServiceMeta = {
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
- cloudbeaver: { label: "CloudBeaver", icon: "assets/database.svg" },
+ cloudbeaver: { label: "CloudBeaver", icon: "assets/cloudbeaver.svg" },
config: { label: "Config", icon: "assets/config.svg" },
gitea: { label: "Gitea", icon: "assets/gitea.svg" },
grafana: { label: "Grafana", icon: "assets/grafana.svg" },
@@ -42,7 +42,7 @@ const defaultServiceMeta = {
ldap: { label: "LDAP", icon: "assets/ldap.svg" },
messages: { label: "Messages", icon: "assets/messages.svg" },
opensearch: { label: "OpenSearch", icon: "assets/opensearch.svg" },
- postgresql: { label: "PostgreSQL", icon: "assets/database.svg" },
+ postgresql: { label: "PostgreSQL", icon: "assets/postgresql.svg" },
prometheus: { label: "Prometheus", icon: "assets/prometheus.svg" },
rabbitmq: { label: "RabbitMQ", icon: "assets/rabbitmq.svg" },
redmine: { label: "Redmine", icon: "assets/redmine.svg" },
@@ -614,9 +614,23 @@ function normalizeItemFromConfig(item, index = 0) {
type: String(item.type),
tags: Array.isArray(item.tags) ? item.tags.map(String) : [],
url: typeof item.url === "string" && item.url.trim() ? item.url.trim() : "#",
+ icon: typeof item.icon === "string" && item.icon.trim() ? item.icon.trim() : "",
};
}
+function getItemMeta(item) {
+ const typeMeta = serviceMeta[item.type] || serviceMeta.config;
+
+ if (item.icon) {
+ return {
+ label: typeMeta.label,
+ icon: item.icon,
+ };
+ }
+
+ return typeMeta;
+}
+
function slugifyItemKey(value) {
return normalize(String(value))
.replace(/[^a-z0-9\u0430-\u044f\u0451]+/g, "-")
@@ -782,7 +796,7 @@ function createGroupElement(group, items, forceOpen = false, query = "") {
// Создает конечную карточку навигации.
// Если url === "#", карточка считается заглушкой и не меняет текущий hash/scroll.
function createCardElement(item, query = "") {
- const meta = serviceMeta[item.type] || serviceMeta.config;
+ const meta = getItemMeta(item);
const link = document.createElement("a");
link.className = "nav-card";
link.href = item.url;
@@ -950,11 +964,16 @@ function createTreeItemList(items, query = "", className = "tree-items") {
itemList.className = className;
items.forEach((item) => {
+ const meta = getItemMeta(item);
const itemButton = document.createElement("button");
itemButton.type = "button";
itemButton.className = "tree-node tree-file";
itemButton.dataset.focusTarget = getItemId(item);
- itemButton.innerHTML = `${highlightText(item.title, query)}${highlightText(item.tags.join(", "), query)}`;
+ itemButton.innerHTML = `
+
+ ${highlightText(item.title, query)}
+ ${highlightText(item.tags.join(", "), query)}
+ `;
itemList.append(itemButton);
});
diff --git a/examples/shared/styles.css b/examples/shared/styles.css
index 597afb4..6831e52 100644
--- a/examples/shared/styles.css
+++ b/examples/shared/styles.css
@@ -325,11 +325,17 @@ html[data-tree-two-column="true"] .tree-items {
}
html[data-tree-two-column="true"] .tree-node.tree-file {
- grid-template-columns: 16px minmax(0, 1fr);
+ grid-template-columns: 20px minmax(0, 1fr);
min-height: 24px;
font-size: 0.8rem;
}
+html[data-tree-two-column="true"] .tree-node-icon {
+ width: 16px;
+ height: 16px;
+ border-radius: 4px;
+}
+
html[data-tree-two-column="true"] .tree-node.tree-file small {
display: none;
}
@@ -930,12 +936,19 @@ html[data-theme="light"] .accent-swatch-custom {
}
.tree-file {
- grid-template-columns: 18px minmax(0, 1fr) auto;
+ grid-template-columns: 20px minmax(0, 1fr) auto;
}
.tree-file::before {
- content: "📄";
- font-size: 0.86rem;
+ content: none;
+}
+
+.tree-node-icon {
+ width: 18px;
+ height: 18px;
+ border-radius: 5px;
+ object-fit: cover;
+ display: block;
}
/* Контейнер элементов внутри группы дерева. */
diff --git a/script.js b/script.js
index 411d0f3..f3f6295 100644
--- a/script.js
+++ b/script.js
@@ -33,7 +33,7 @@ let panelTitle = "Панель экосистемы";
const defaultServiceMeta = {
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
- cloudbeaver: { label: "CloudBeaver", icon: "assets/database.svg" },
+ cloudbeaver: { label: "CloudBeaver", icon: "assets/cloudbeaver.svg" },
config: { label: "Config", icon: "assets/config.svg" },
gitea: { label: "Gitea", icon: "assets/gitea.svg" },
grafana: { label: "Grafana", icon: "assets/grafana.svg" },
@@ -42,7 +42,7 @@ const defaultServiceMeta = {
ldap: { label: "LDAP", icon: "assets/ldap.svg" },
messages: { label: "Messages", icon: "assets/messages.svg" },
opensearch: { label: "OpenSearch", icon: "assets/opensearch.svg" },
- postgresql: { label: "PostgreSQL", icon: "assets/database.svg" },
+ postgresql: { label: "PostgreSQL", icon: "assets/postgresql.svg" },
prometheus: { label: "Prometheus", icon: "assets/prometheus.svg" },
rabbitmq: { label: "RabbitMQ", icon: "assets/rabbitmq.svg" },
redmine: { label: "Redmine", icon: "assets/redmine.svg" },
@@ -614,9 +614,23 @@ function normalizeItemFromConfig(item, index = 0) {
type: String(item.type),
tags: Array.isArray(item.tags) ? item.tags.map(String) : [],
url: typeof item.url === "string" && item.url.trim() ? item.url.trim() : "#",
+ icon: typeof item.icon === "string" && item.icon.trim() ? item.icon.trim() : "",
};
}
+function getItemMeta(item) {
+ const typeMeta = serviceMeta[item.type] || serviceMeta.config;
+
+ if (item.icon) {
+ return {
+ label: typeMeta.label,
+ icon: item.icon,
+ };
+ }
+
+ return typeMeta;
+}
+
function slugifyItemKey(value) {
return normalize(String(value))
.replace(/[^a-z0-9\u0430-\u044f\u0451]+/g, "-")
@@ -782,7 +796,7 @@ function createGroupElement(group, items, forceOpen = false, query = "") {
// Создает конечную карточку навигации.
// Если url === "#", карточка считается заглушкой и не меняет текущий hash/scroll.
function createCardElement(item, query = "") {
- const meta = serviceMeta[item.type] || serviceMeta.config;
+ const meta = getItemMeta(item);
const link = document.createElement("a");
link.className = "nav-card";
link.href = item.url;
@@ -950,11 +964,16 @@ function createTreeItemList(items, query = "", className = "tree-items") {
itemList.className = className;
items.forEach((item) => {
+ const meta = getItemMeta(item);
const itemButton = document.createElement("button");
itemButton.type = "button";
itemButton.className = "tree-node tree-file";
itemButton.dataset.focusTarget = getItemId(item);
- itemButton.innerHTML = `${highlightText(item.title, query)}${highlightText(item.tags.join(", "), query)}`;
+ itemButton.innerHTML = `
+
+ ${highlightText(item.title, query)}
+ ${highlightText(item.tags.join(", "), query)}
+ `;
itemList.append(itemButton);
});
diff --git a/styles.css b/styles.css
index 597afb4..6831e52 100644
--- a/styles.css
+++ b/styles.css
@@ -325,11 +325,17 @@ html[data-tree-two-column="true"] .tree-items {
}
html[data-tree-two-column="true"] .tree-node.tree-file {
- grid-template-columns: 16px minmax(0, 1fr);
+ grid-template-columns: 20px minmax(0, 1fr);
min-height: 24px;
font-size: 0.8rem;
}
+html[data-tree-two-column="true"] .tree-node-icon {
+ width: 16px;
+ height: 16px;
+ border-radius: 4px;
+}
+
html[data-tree-two-column="true"] .tree-node.tree-file small {
display: none;
}
@@ -930,12 +936,19 @@ html[data-theme="light"] .accent-swatch-custom {
}
.tree-file {
- grid-template-columns: 18px minmax(0, 1fr) auto;
+ grid-template-columns: 20px minmax(0, 1fr) auto;
}
.tree-file::before {
- content: "📄";
- font-size: 0.86rem;
+ content: none;
+}
+
+.tree-node-icon {
+ width: 18px;
+ height: 18px;
+ border-radius: 5px;
+ object-fit: cover;
+ display: block;
}
/* Контейнер элементов внутри группы дерева. */