Lagebild: Einheiten als verbundene Karten, Strukturbaum mit Suche und Filter (#711)

Rueckmeldung zur Teamuebersicht: wirkte veraltet, und die Verbindungen
zwischen den Einheiten fehlten - zugeklappte Container versteckten ganze
Ebenen, man haette sich bis zum Problem durchklicken muessen.

- Jede Einheit ist immer eine Karte, verbunden als Organisationsbaum
  (ELK layered, orthogonale Linien, eckige Anschluesse). Keine Ebene wird
  versteckt. Zugeklappt wird nur die Agentenliste grosser Einheiten: sie
  zeigt, wer Aufmerksamkeit braucht, und "+n weitere".
- Eine Linie traegt die Farbe des dringlichsten Zustands darunter - eine
  rote Linie fuehrt von oben zum Problem.
- Links ein Strukturbaum mit Suche (Agent, Einheit, Modell) und
  Zustandsfilter; Treffer werden hervorgehoben, der Weg dorthin geoeffnet,
  der Rest abgeblendet. Enter springt zum ersten Treffer.
- Leinwand mit Punktraster, Verschieben und Zoomen, schwebende Leisten
  (Live-Verbindungen, Einpassen, Zoom). Live-Verbindungen laufen von Zeile
  zu Zeile, gebuendelt mit Zaehler.
- Hausstil bleibt (Radius 0, App-Akzent), hell und dunkel, bis Handybreite.
- Cytoscape und cytoscape-elk entfernt: ein Canvas-Knoten kann keine
  Agentenliste, Suche oder den Hausstil tragen. Karten sind jetzt HTML.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-24 16:38:51 +02:00
co-authored by Claude Opus 5.5
parent 0718a4bd0d
commit b2b8a34d9f
10 changed files with 1439 additions and 914 deletions
+464 -25
View File
@@ -1722,7 +1722,9 @@ td.klein {
font-size: 12px;
}
/* ─── Team overview (#711) ─── */
/* ─── Team overview (#711) ───
A work surface in the blueprint style: dot grid, cards with a head, state stripes,
square ports on the lines, floating bars. Square like everything else. */
.team-ansicht {
display: flex;
@@ -1731,45 +1733,474 @@ td.klein {
height: 100%;
}
.team-kopf {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.team-kopf h1 { margin: 0; }
.team-kopf p { margin: 4px 0 0; max-width: 72ch; }
.team-zusammenfassung {
flex-wrap: wrap;
min-height: 24px;
}
.team-flaeche {
display: flex;
gap: 12px;
flex: 1;
min-height: 420px;
/* Tones: every state has a colour, shared by markers, chips, lines and cards. */
.ton-neutral { --ton: var(--gedaempft); }
.ton-aktiv { --ton: var(--accent); }
.ton-warnung { --ton: var(--warnung); }
.ton-gefahr { --ton: var(--gefahr); }
.team-marker {
color: var(--ton, var(--gedaempft));
font-size: 12px;
text-align: center;
}
.team-graph {
.team-arbeitsflaeche {
flex: none;
/* A fixed height: the tree and the panel scroll inside it, the page does not grow. */
height: max(520px, calc(100vh - 250px));
display: grid;
grid-template-columns: 264px minmax(0, 1fr) auto;
grid-template-rows: minmax(0, 1fr);
border: 1px solid var(--divider);
background: var(--surface);
}
/* The tree on the left */
.team-baum {
display: flex;
flex-direction: column;
min-height: 0;
border-right: 1px solid var(--divider);
background: var(--surface);
}
.team-baum-kopf {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
border-bottom: 1px solid var(--divider);
}
.team-baum-titel {
font-family: var(--schrift-titel);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--gedaempft);
}
.team-suche {
width: 100%;
padding: 7px 10px;
}
.team-filter {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.team-filter button {
padding: 2px 8px;
font-size: 12px;
background: transparent;
color: var(--gedaempft);
}
.team-filter button[aria-pressed="true"] {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.team-treffer-info {
min-height: 16px;
font-size: 12px;
color: var(--gedaempft);
}
.team-baum-liste {
flex: 1;
overflow: auto;
padding: 6px 0;
}
.team-baum-liste ul {
list-style: none;
margin: 0;
padding: 0;
}
.team-baum-eintrag {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 6px;
width: 100%;
padding: 4px 10px 4px calc(8px + var(--tiefe, 0) * 14px);
background: transparent;
border: 0;
font-size: 13px;
text-align: left;
}
.team-baum-eintrag.ausgewaehlt {
background: color-mix(in srgb, var(--accent) 18%, transparent);
box-shadow: inset 2px 0 0 var(--accent);
}
.team-baum-eintrag.treffer .team-baum-name {
color: var(--accent);
font-weight: 600;
}
.team-baum-eintrag.gedimmt { opacity: 0.45; }
.team-pfeil {
flex: none;
width: 14px;
font-size: 10px;
color: var(--gedaempft);
}
.team-baum-name {
flex: 1;
min-width: 0;
min-height: 420px;
background: var(--surface-subtil);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.team-zahl {
flex: none;
font-size: 11px;
color: var(--gedaempft);
font-variant-numeric: tabular-nums;
}
.team-zahl.ton-aktiv,
.team-zahl.ton-warnung,
.team-zahl.ton-gefahr { color: var(--ton); }
/* The canvas */
.team-leinwand {
position: relative;
overflow: hidden;
min-width: 0;
cursor: grab;
touch-action: none;
user-select: none;
background-color: var(--bg);
background-image: radial-gradient(circle, var(--divider) 1px, transparent 1.3px);
background-size: 20px 20px;
}
.team-leinwand.zieht { cursor: grabbing; }
.team-welt {
position: absolute;
left: 0;
top: 0;
transform-origin: 0 0;
}
.team-welt.gleitend { transition: transform 0.35s ease; }
.team-kanten,
.team-links {
position: absolute;
left: 0;
top: 0;
overflow: visible;
pointer-events: none;
}
.team-links { z-index: 1; }
.team-kante {
fill: none;
stroke: color-mix(in srgb, var(--ton) 55%, var(--divider));
stroke-width: 1.5;
}
.team-kante.ton-gefahr,
.team-kante.ton-warnung {
stroke: var(--ton);
stroke-width: 2;
}
.team-kante.ton-gefahr {
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--gefahr) 70%, transparent));
}
.team-port {
fill: var(--bg);
stroke: color-mix(in srgb, var(--ton) 70%, var(--divider));
stroke-width: 1.5;
}
.team-port.ton-gefahr,
.team-port.ton-warnung { fill: var(--ton); stroke: var(--ton); }
.team-kante.gedimmt,
.team-port.gedimmt { opacity: 0.2; }
.team-link {
fill: none;
stroke: var(--accent);
stroke-width: 2;
stroke-dasharray: 6 5;
animation: team-fluss 0.9s linear infinite;
}
.team-pfeilspitze { fill: var(--accent); }
@keyframes team-fluss {
to { stroke-dashoffset: -11; }
}
.team-link-zahl {
font-family: var(--schrift-text);
font-size: 12px;
font-weight: 600;
fill: var(--text);
paint-order: stroke;
stroke: var(--bg);
stroke-width: 4px;
}
/* Cards */
.team-karte {
position: absolute;
left: 0;
top: 0;
background: var(--surface);
border: 1px solid var(--divider);
border-left: 3px solid var(--ton, var(--divider));
box-shadow: 0 12px 28px -16px rgba(0, 0, 0, 0.55);
transition: opacity 0.2s ease, box-shadow 0.2s ease;
}
.team-karte.ton-neutral { border-left-color: var(--divider); }
.team-karte.ton-gefahr {
border-color: color-mix(in srgb, var(--gefahr) 60%, var(--divider));
border-left-color: var(--gefahr);
box-shadow:
0 0 0 1px color-mix(in srgb, var(--gefahr) 35%, transparent),
0 0 26px -6px color-mix(in srgb, var(--gefahr) 60%, transparent);
}
.team-karte.ton-warnung {
border-color: color-mix(in srgb, var(--warnung) 45%, var(--divider));
border-left-color: var(--warnung);
}
.team-karte.wurzel .team-karte-kopf {
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
}
.team-karte.ausgewaehlt,
.team-karte.treffer {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
.team-karte.gedimmt { opacity: 0.3; }
.team-karte-kopf {
display: block;
width: 100%;
padding: 10px 12px 9px;
background: transparent;
border: 0;
border-bottom: 1px solid var(--divider);
text-align: left;
}
.team-karte-titel {
display: flex;
align-items: baseline;
gap: 8px;
font-family: var(--schrift-titel);
font-size: 17px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.team-karte-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.team-karte-unter {
margin-top: 2px;
font-size: 12px;
color: var(--gedaempft);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.team-chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 8px;
}
.team-chip {
padding: 1px 6px;
font-size: 11px;
font-variant-numeric: tabular-nums;
color: var(--gedaempft);
border: 1px solid var(--divider);
}
.team-chip.ton-aktiv,
.team-chip.ton-warnung,
.team-chip.ton-gefahr {
color: var(--ton);
border-color: color-mix(in srgb, var(--ton) 55%, transparent);
background: color-mix(in srgb, var(--ton) 12%, transparent);
}
.team-karte-liste { padding: 4px 0; }
.team-zeile {
display: grid;
grid-template-columns: 16px minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
width: 100%;
padding: 5px 12px;
background: transparent;
border: 0;
font-size: 13px;
text-align: left;
}
.team-zeile.ausgewaehlt { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.team-zeile.treffer { box-shadow: inset 2px 0 0 var(--accent); }
.team-zeile.gedimmt { opacity: 0.4; }
.team-zeile.blinkt { animation: team-blinkt 1.4s ease; }
@keyframes team-blinkt {
0%, 55% { background: color-mix(in srgb, var(--accent) 35%, transparent); }
100% { background: transparent; }
}
.team-zeile-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.team-zeile-zustand {
font-size: 11px;
color: var(--gedaempft);
}
.team-zeile.ton-aktiv .team-zeile-zustand,
.team-zeile.ton-warnung .team-zeile-zustand,
.team-zeile.ton-gefahr .team-zeile-zustand { color: var(--ton); }
.team-mehr {
display: block;
width: 100%;
padding: 4px 12px 7px 36px;
background: transparent;
border: 0;
font-size: 12px;
text-align: left;
color: var(--accent);
}
.team-mehr:hover:not(:disabled) {
background: transparent;
text-decoration: underline;
}
/* Floating bars */
.team-leiste {
position: absolute;
z-index: 2;
display: flex;
gap: 2px;
padding: 4px;
background: var(--surface);
border: 1px solid var(--divider);
box-shadow: 0 12px 30px -14px rgba(0, 0, 0, 0.6);
}
.team-leiste button {
padding: 6px 12px;
background: transparent;
border: 0;
font-size: 13px;
}
.team-leiste button[aria-pressed="true"] {
background: color-mix(in srgb, var(--accent) 20%, transparent);
box-shadow: inset 0 -2px 0 var(--accent);
}
.team-leiste-mitte {
left: 50%;
bottom: 16px;
transform: translateX(-50%);
}
.team-leiste-zoom {
right: 16px;
bottom: 16px;
}
.team-zoomwert {
align-self: center;
min-width: 48px;
font-size: 12px;
text-align: center;
color: var(--gedaempft);
font-variant-numeric: tabular-nums;
}
/* Side panel */
.team-seitenleiste {
width: 320px;
flex-shrink: 0;
padding: 14px;
background: var(--surface);
border: 1px solid var(--divider);
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
background: var(--surface);
border-left: 1px solid var(--divider);
}
.team-seitenleiste-kopf {
justify-content: space-between;
}
.team-seitenleiste[hidden] { display: none; }
.team-seitenleiste h3 {
margin: 8px 0 0;
}
.team-seitenleiste-kopf { justify-content: space-between; }
.team-seitenleiste h2 { margin: 0; }
.team-seitenleiste h3 { margin: 8px 0 0; }
.team-seitenleiste .team-zeile { padding: 5px 4px; }
.team-ergebnis {
white-space: pre-wrap;
@@ -1786,20 +2217,28 @@ td.klein {
margin-top: 8px;
}
.team-eintrag {
justify-content: flex-start;
width: 100%;
text-align: left;
}
.team-legende {
flex-wrap: wrap;
gap: 16px;
}
.team-eintrag {
text-align: left;
justify-content: flex-start;
width: 100%;
@media (prefers-reduced-motion: reduce) {
.team-link,
.team-zeile.blinkt { animation: none; }
.team-welt.gleitend { transition: none; }
}
@media (max-width: 800px) {
.team-flaeche { flex-direction: column; }
.team-seitenleiste { width: auto; }
@media (max-width: 900px) {
.team-arbeitsflaeche { height: auto; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto; }
.team-baum { max-height: 280px; border-right: 0; border-bottom: 1px solid var(--divider); }
.team-leinwand { min-height: 60vh; }
.team-seitenleiste { width: auto; border-left: 0; border-top: 1px solid var(--divider); }
}
/* ─── Organisation (#683) ─── */
@@ -128,10 +128,10 @@
"org.toolsLostConfirm": "Werkzeuge entfernen und übernehmen",
"org.runningRefusal": "Nicht, solange {agents} läuft — ein Agent darf nicht mitten im Lauf andere Rechte bekommen.",
"team.title": "Teamübersicht",
"team.subtitle": "Wer arbeitet, wer hat wen in den letzten Sekunden erreicht und wo klemmt es. Einheiten zeigen, was in ihnen passiert; ein Klick öffnet sie oder zeigt Einzelheiten.",
"team.subtitle": "Jede Einheit ist eine Karte, verbunden wie die Organisation. Eine Linie trägt die Farbe des dringlichsten Zustands darunter, eine rote Linie führt also zum Problem. Klick auf Karte oder Agent zeigt Einzelheiten.",
"team.noInstance": "Keine Instanz ausgewählt. Oben eine aktive Instanz wählen.",
"team.empty": "Diese Instanz hat keine Agenten.",
"team.legendLink": "hat erreicht (verblasst nach 30 s)",
"team.legendLink": "Live-Verbindung (verblasst nach 30 s)",
"team.state.Idle": "bereit",
"team.state.Working": "arbeitet",
"team.state.Stuck": "hängt",
@@ -151,14 +151,33 @@
"team.unitAgentsOne": "1 Agent",
"team.unitAgents": "{n} Agenten",
"team.legendLead": "Leitung der Einheit",
"team.legendUnit": "Einheit, Klick öffnet sie",
"team.leads": "Leitet",
"team.memberOf": "Mitglied von",
"team.members": "Mitglieder",
"team.subUnits": "Untereinheiten",
"team.collapse": "Einheit zuklappen",
"team.expand": "Einheit aufklappen",
"team.openOrganisation": "Organisation bearbeiten",
"team.rootSubtitle": "Instanz · oberste Ebene",
"team.structure": "Struktur",
"team.searchPlaceholder": "Agenten, Einheiten, Modelle suchen…",
"team.filterLabel": "Nach Zustand filtern",
"team.filter.all": "Alle",
"team.filter.problems": "Probleme",
"team.filter.waiting": "Freigaben",
"team.filter.working": "Arbeiten",
"team.filter.budget": "Budget",
"team.needsAttention": "{n} brauchen Aufmerksamkeit",
"team.matches": "{n} Treffer",
"team.matchesOne": "1 Treffer",
"team.noMatches": "Keine Treffer",
"team.moreMembers": "+{n} weitere",
"team.showFewer": "Weniger zeigen",
"team.showAllMembers": "Alle Mitglieder zeigen",
"team.zoomIn": "Vergrößern",
"team.zoomOut": "Verkleinern",
"team.fit": "Einpassen",
"team.liveLinks": "Live-Verbindungen",
"team.subtreeHint": "Die Zahlen umfassen alle Einheiten darunter.",
"team.legendLine": "Linie = dringlichster Zustand darunter",
"nav.services": "Dienste",
"nav.group.analytics": "Analyse",
"nav.usage": "Tokennutzung",
@@ -128,10 +128,10 @@
"org.toolsLostConfirm": "Remove the tools and apply",
"org.runningRefusal": "Not while {agents} is running — an agent must not get different rights in the middle of a run.",
"team.title": "Team overview",
"team.subtitle": "Who is working, who reached whom in the last seconds, and where something is stuck. Units show what happens inside them; click to open them or to see details.",
"team.subtitle": "Every unit is a card, connected as the organisation. A line takes the colour of the most urgent state below it, so a red line leads to the problem. Click a card or an agent for details.",
"team.noInstance": "No instance selected. Choose an active instance in the header.",
"team.empty": "No agents in this instance.",
"team.legendLink": "reached (fades after 30 s)",
"team.legendLink": "live link (fades after 30 s)",
"team.state.Idle": "idle",
"team.state.Working": "working",
"team.state.Stuck": "stuck",
@@ -151,14 +151,33 @@
"team.unitAgentsOne": "1 agent",
"team.unitAgents": "{n} agents",
"team.legendLead": "lead of the unit",
"team.legendUnit": "unit, click to open",
"team.leads": "Leads",
"team.memberOf": "Member of",
"team.members": "Members",
"team.subUnits": "Sub-units",
"team.collapse": "Collapse unit",
"team.expand": "Expand unit",
"team.openOrganisation": "Edit organisation",
"team.rootSubtitle": "Instance · top level",
"team.structure": "Structure",
"team.searchPlaceholder": "Search agents, units, models…",
"team.filterLabel": "Filter by state",
"team.filter.all": "All",
"team.filter.problems": "Problems",
"team.filter.waiting": "Approvals",
"team.filter.working": "Working",
"team.filter.budget": "Budget",
"team.needsAttention": "{n} need attention",
"team.matches": "{n} matches",
"team.matchesOne": "1 match",
"team.noMatches": "No matches",
"team.moreMembers": "+{n} more",
"team.showFewer": "Show fewer",
"team.showAllMembers": "Show all members",
"team.zoomIn": "Zoom in",
"team.zoomOut": "Zoom out",
"team.fit": "Fit",
"team.liveLinks": "Live links",
"team.subtreeHint": "Counts include all units below.",
"team.legendLine": "line = most urgent state below",
"nav.services": "Services",
"nav.group.analytics": "Analytics",
"nav.usage": "Token Usage",
+11 -7
View File
@@ -9,15 +9,19 @@ Downloaded 2026-09-24 from the npm registry. Each tarball was checked against th
| Package | Version | File | License | SHA-256 of the file |
|---|---|---|---|---|
| [cytoscape](https://www.npmjs.com/package/cytoscape) | 3.34.3 | `cytoscape/cytoscape.min.js` | MIT | `5f3b5b529546d5af1fc5628590af033b74511a5b6f789f5f4682845863228b91` |
| [elkjs](https://www.npmjs.com/package/elkjs) | 0.9.3 | `elkjs/elk.bundled.js` | EPL-2.0 | `b0745abd7f23cd91690a1587e377edbe19fd7233c783300290936720546216d4` |
| [cytoscape-elk](https://www.npmjs.com/package/cytoscape-elk) | 2.3.0 | `cytoscape-elk/cytoscape-elk.js` | MIT | `25acbb9dea9b0dc6fef0e79b697547ee56e579f0e1c22ae4353131e72c7e140a` |
**Why elkjs 0.9.3 and not the newest.** cytoscape-elk 2.3.0 declares `elkjs ^0.9.3`. A newer
elkjs would be untested with the adapter.
**Why elkjs 0.9.3 and not the newest.** It came in together with cytoscape-elk 2.3.0,
which declared `elkjs ^0.9.3`. Cytoscape and the adapter were removed again on 2026-09-24
(see below); elkjs stays at the version it was checked at. Raising it is a separate change.
**How they are loaded.** All three are UMD bundles and, loaded as classic scripts in this
order, register the globals `cytoscape`, `ELK` and `cytoscapeElk`. The team overview (#711)
loads them on demand, so pages that do not draw a graph do not pay for ~2 MB of script.
**Why Cytoscape is gone.** The team overview (#711) first drew its graph with Cytoscape.js
on a canvas. A canvas node cannot hold a list of agents, a search highlight or the house
style, so the cards became plain HTML placed by ELK, with the lines in SVG. Nothing else
used Cytoscape.
**How it is loaded.** `elk.bundled.js` is a UMD bundle and, loaded as a classic script,
registers the global `ELK`. The team overview loads it on demand, so pages that do not draw
the organisation do not pay for ~1.5 MB of script.
The license texts sit next to each file.
@@ -1,21 +0,0 @@
Copyright (c) 2016-2018, 2020, The Cytoscape Consortium.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,356 +0,0 @@
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(require("elkjs/lib/elk.bundled.js"));
else if(typeof define === 'function' && define.amd)
define(["elkjs"], factory);
else if(typeof exports === 'object')
exports["cytoscapeElk"] = factory(require("elkjs/lib/elk.bundled.js"));
else
root["cytoscapeElk"] = factory(root["ELK"]);
})(this, function(__WEBPACK_EXTERNAL_MODULE__632__) {
return /******/ (function() { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ 632:
/***/ (function(module) {
module.exports = __WEBPACK_EXTERNAL_MODULE__632__;
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ !function() {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function() { return module['default']; } :
/******/ function() { return module; };
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ !function() {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = function(exports, definition) {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ !function() {
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
/******/ }();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
!function() {
// EXPORTS
__webpack_require__.d(__webpack_exports__, {
"default": function() { return /* binding */ src; }
});
// EXTERNAL MODULE: external {"commonjs":"elkjs/lib/elk.bundled.js","commonjs2":"elkjs/lib/elk.bundled.js","amd":"elkjs","root":"ELK"}
var elk_bundled_js_amd_elkjs_root_ELK_ = __webpack_require__(632);
var elk_bundled_js_amd_elkjs_root_ELK_default = /*#__PURE__*/__webpack_require__.n(elk_bundled_js_amd_elkjs_root_ELK_);
;// CONCATENATED MODULE: ./src/assign.js
// Simple, internal Object.assign() polyfill for options objects etc.
function assign_assign(tgt) {
for (var _len = arguments.length, srcs = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
srcs[_key - 1] = arguments[_key];
}
srcs.forEach(function (src) {
Object.keys(src).forEach(function (k) {
return tgt[k] = src[k];
});
});
return tgt;
}
/* harmony default export */ var src_assign = (Object.assign != null ? Object.assign.bind(Object) : assign_assign);
;// CONCATENATED MODULE: ./src/defaults.js
var defaults = {
nodeDimensionsIncludeLabels: false,
// Boolean which changes whether label dimensions are included when calculating node dimensions
fit: true,
// Whether to fit
padding: 20,
// Padding on fit
animate: false,
// Whether to transition the node positions
animateFilter: function animateFilter() {
return true;
},
// Whether to animate specific nodes when animation is on; non-animated nodes immediately go to their final positions
animationDuration: 500,
// Duration of animation in ms if enabled
animationEasing: undefined,
// Easing of animation if enabled
transform: function transform(node, pos) {
return pos;
},
// A function that applies a transform to the final node position
ready: undefined,
// Callback on layoutready
stop: undefined,
// Callback on layoutstop
nodeLayoutOptions: undefined,
// Special options for only the nodes
elk: {
// Options to pass directly to ELK `layoutOptions`. The subsequent identifier has to be used as property key in quotes.
// E.g. for 'org.eclipse.elk.direction' use:
// 'elk.direction'
// Primary/mandatory, the elk algorithm to use
// one of 'box', 'disco', 'force', 'layered', 'mrtree', 'radial', 'random', 'stress'
// (see https://www.eclipse.org/elk/reference/algorithms.html)
algorithm: undefined
},
priority: function priority() {
return null;
} // Edges with a non-nil value are skipped when geedy edge cycle breaking is enabled
};
/* harmony default export */ var src_defaults = (defaults);
;// CONCATENATED MODULE: ./src/layout.js
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
var elkOverrides = {};
var getPos = function getPos(ele, options) {
var dims = ele.layoutDimensions(options);
var parent = ele.parent();
var k = ele.scratch('elk');
var p = {
x: k.x,
y: k.y
};
while (parent.nonempty()) {
var kp = parent.scratch('elk');
p.x += kp.x;
p.y += kp.y;
parent = parent.parent();
} // elk considers a node position to be its top-left corner, while cy is the centre
p.x += dims.w / 2;
p.y += dims.h / 2;
return p;
};
var makeNode = function makeNode(node, options) {
var k = {
_cyEle: node,
id: node.id()
}; // Apply nodeLayoutOptions per user-specified function
// e.g. nodeLayoutOptions => n.scratch('layoutOptions')
if (options.nodeLayoutOptions) {
k.layoutOptions = options.nodeLayoutOptions(node);
}
if (!node.isParent()) {
var dims = node.layoutDimensions(options);
var p = node.position(); // the elk position is the top-left corner, cy is the centre
k.x = p.x - dims.w / 2;
k.y = p.y - dims.h / 2;
k.width = dims.w;
k.height = dims.h;
}
node.scratch('elk', k);
return k;
};
var makeEdge = function makeEdge(edge
/*, options*/
) {
var k = {
_cyEle: edge,
id: edge.id(),
source: edge.data('source'),
target: edge.data('target')
};
edge.scratch('elk', k);
return k;
};
var makeGraph = function makeGraph(nodes, edges, options) {
var elkNodes = [];
var elkEdges = [];
var elkEleLookup = {};
var graph = {
id: 'root',
children: [],
edges: []
}; // map all nodes
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i];
var k = makeNode(n, options);
elkNodes.push(k);
elkEleLookup[n.id()] = k;
} // map all edges
for (var _i = 0; _i < edges.length; _i++) {
var e = edges[_i];
var _k = makeEdge(e, options);
elkEdges.push(_k);
elkEleLookup[e.id()] = _k;
} // make hierarchy
for (var _i2 = 0; _i2 < elkNodes.length; _i2++) {
var _k2 = elkNodes[_i2];
var _n = _k2._cyEle;
if (!_n.isChild()) {
graph.children.push(_k2);
} else {
var parent = _n.parent();
var parentK = elkEleLookup[parent.id()];
var children = parentK.children = parentK.children || [];
children.push(_k2);
}
}
for (var _i3 = 0; _i3 < elkEdges.length; _i3++) {
var _k3 = elkEdges[_i3]; // put all edges in the top level for now
// TODO does this cause issues in certain edgecases?
/*let e = k._cyEle;
let parentSrc = e.source().parent();
let parentTgt = e.target().parent();
if ( false && parentSrc.nonempty() && parentTgt.nonempty() && parentSrc.same( parentTgt ) ){
let kp = elkEleLookup[ parentSrc.id() ];
kp.edges = kp.edges || [];
kp.edges.push( k );
} else {*/
graph.edges.push(_k3); //}
}
return graph;
};
var Layout = /*#__PURE__*/function () {
function Layout(options) {
_classCallCheck(this, Layout);
var elkOptions = options.elk;
var cy = options.cy;
this.options = src_assign({}, src_defaults, options);
this.options.elk = src_assign({
aspectRatio: cy.width() / cy.height()
}, src_defaults.elk, elkOptions, elkOverrides);
}
_createClass(Layout, [{
key: "run",
value: function run() {
var layout = this;
var options = this.options;
var eles = options.eles;
var nodes = eles.nodes();
var edges = eles.edges();
var elk = new (elk_bundled_js_amd_elkjs_root_ELK_default())();
var graph = makeGraph(nodes, edges, options);
graph['layoutOptions'] = options.elk;
elk.layout(graph).then(function () {
nodes.filter(function (n) {
return !n.isParent();
}).layoutPositions(layout, options, function (n) {
return getPos(n, options);
});
});
return this;
}
}, {
key: "stop",
value: function stop() {
return this; // chaining
}
}, {
key: "destroy",
value: function destroy() {
return this; // chaining
}
}]);
return Layout;
}();
/* harmony default export */ var layout = (Layout);
;// CONCATENATED MODULE: ./src/index.js
// registers the extension on a cytoscape lib ref
var register = function register(cytoscape) {
if (!cytoscape) {
return;
} // can't register if cytoscape unspecified
cytoscape('layout', 'elk', layout); // register with cytoscape.js
};
if (typeof cytoscape !== 'undefined') {
// expose to global cytoscape (i.e. window.cytoscape)
// eslint-disable-next-line no-undef
register(cytoscape);
}
/* harmony default export */ var src = (register);
}();
__webpack_exports__ = __webpack_exports__["default"];
/******/ return __webpack_exports__;
/******/ })()
;
});
@@ -1,19 +0,0 @@
Copyright (c) 2016-2026, The Cytoscape Consortium.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff