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; } /* Контейнер элементов внутри группы дерева. */