hmgr 0.0.44 → 0.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -745,7 +745,7 @@
745
745
  }
746
746
 
747
747
  /** Eine Zeile der Link-Tabelle. `bundle` gesetzt heißt: die Zeile steht in einer Bundle-Gruppe
748
- (E69), das Entfernen zerbricht dort das Bundle und der Dialog sagt es. */
748
+ (E69), das Entfernen nimmt den Skill dort aus dem Bundle heraus und der Dialog sagt es (E114). */
749
749
  function linkZeile(ei, z, bundle) {
750
750
  var name = ei.art === 'link' ? ei.link.name : ei.name;
751
751
  var li = e('li');
@@ -801,63 +801,156 @@
801
801
  return li;
802
802
  }
803
803
 
804
- /** Ein installiertes Bundle als Gruppe über seinen Skill-Zeilen (E69, F68): Kopf mit Name, Quelle
805
- und „Entfernen" für das Ganze, darunter die Zeilen seiner Mitglieder. */
806
- function bundleGruppe(b, z, eintraege) {
804
+ /** Alle Mitglieder eines installierten Bundles in einem Ziel (E114 L3): verlinkte und nicht
805
+ verlinkte, aus dem Zustand — ohne die Definition, die Gruppe steht auch bei fehlender Quelle. */
806
+ function bundleAlleMitglieder(b) {
807
+ return b.skills.concat(b.nichtVerlinkt || []);
808
+ }
809
+
810
+ /** Skill zu einem Mitglied einer Definition aus dem Skill-Katalog (E114 L1): gleiche Quelle, gleicher
811
+ Pfad. null, wenn die Quelle (noch) nicht eingerichtet oder nicht lesbar ist. */
812
+ function bundleMitgliedSkill(m) {
813
+ return SKILLS.filter(function (s) { return s.quelleSchluessel === m.quelleSchluessel && s.pfad === m.pfad; })[0] || null;
814
+ }
815
+
816
+ /** Skill eines nicht verlinkten Mitglieds über seinen Namen, gesucht unter den Mitgliedern der
817
+ Definition (Name plus Quelle des Bundles). null, wenn sich das nicht auflösen lässt. */
818
+ function bundleSkillNachName(b, name) {
819
+ var katalog = BUNDLES.filter(function (k) { return k.name === b.name && k.quelleText === b.quelleText; })[0];
820
+ if (!katalog) return null;
821
+ for (var i = 0; i < katalog.mitglieder.length; i++) {
822
+ var skill = bundleMitgliedSkill(katalog.mitglieder[i]);
823
+ if (skill && skill.name === name) return skill;
824
+ }
825
+ return null;
826
+ }
827
+
828
+ /** Vervollständigen (E114 L4, `aufnehmen` 'alle') oder Wieder verlinken (L3, Namensliste) eines
829
+ Bundles in den genannten Zielen. Ein Fehlschlag in einem Ziel hält die übrigen nicht auf,
830
+ `rufe` meldet ihn selbst. */
831
+ async function bundleAufnehmen(b, ziele, aufnehmen) {
832
+ var gelegt = 0;
833
+ var gescheitert = 0;
834
+ for (var i = 0; i < ziele.length; i++) {
835
+ var ziel = ziele[i];
836
+ try {
837
+ var ergebnis = await rufe(function () {
838
+ return window.hmgr.bundleAbgleichen({
839
+ bundle: b.name, quelleText: b.quelleText, harness: ziel.harness, ort: ziel.ort, aufnehmen: aufnehmen,
840
+ });
841
+ });
842
+ gelegt += ergebnis.gelegt.length;
843
+ } catch (fehler) {
844
+ gescheitert++;
845
+ }
846
+ }
847
+ await ladeAlles();
848
+ if (gescheitert === 0) melde('Bundle ' + b.name + ': ' + zahlwort(gelegt, 'Link', 'Links') + ' gelegt.');
849
+ }
850
+
851
+ // Aufklappzustand der Bundle-Gruppen (E114 L1), überlebt den Neuaufbau durch ladeAlles wie
852
+ // SKILL_OFFEN. Zu Beginn sind die Gruppen zugeklappt.
853
+ var BUNDLE_OFFEN = {};
854
+ var bundleKoerperNr = 0;
855
+
856
+ /** Rahmen einer Bundle-Gruppe (E69, E114 L1/L3/L4): Kopf mit Name, Quelle, „x von N Skills",
857
+ „Vervollständigen", wenn etwas fehlt, und „Entfernen" für das Ganze; darunter aufklappbar die
858
+ Zeilen aller Mitglieder. `harnesse` nur unter „Alle" (Harness-Zeichen am Kopf, E95). */
859
+ function bundleRahmen(b, harnesse, verlinkt, gesamt, ziele, zeilen, entfernen, schluessel) {
807
860
  var gruppe = e('div', 'bundlegruppe');
808
861
  var kopf = e('div', 'bundlekopf');
809
862
  var titel = e('span', 'bundletitel');
863
+ if (harnesse) titel.appendChild(harnesseZeichenSammel(harnesse));
810
864
  titel.appendChild(e('span', 'etikett', 'Bundle'));
811
865
  titel.appendChild(e('b', 'bundlename', b.name));
812
866
  kopf.appendChild(titel);
813
867
  var quelleEl = e('span', 'mono bundlequelle', b.quelleName);
814
868
  quelleEl.title = b.quelleText;
815
869
  kopf.appendChild(quelleEl);
816
- kopf.appendChild(e('span', 'bundlezahl', zahlwort(b.skills.length, 'Skill', 'Skills')));
870
+ kopf.appendChild(e('span', 'bundlezahl', verlinkt === gesamt
871
+ ? zahlwort(gesamt, 'Skill', 'Skills')
872
+ : verlinkt + ' von ' + zahlwort(gesamt, 'Skill', 'Skills')));
873
+ var aktionen = e('span', 'bundleaktionen');
874
+ if (verlinkt < gesamt) {
875
+ var voll = e('button', 'knopf leise klein', 'Vervollständigen');
876
+ voll.type = 'button';
877
+ voll.setAttribute('aria-label', 'Bundle ' + b.name + ' vervollständigen: alle fehlenden Skills verlinken');
878
+ voll.addEventListener('click', function () { bundleAufnehmen(b, ziele, 'alle'); });
879
+ aktionen.appendChild(voll);
880
+ }
817
881
  var weg = e('button', 'knopf leise klein', 'Entfernen');
818
882
  weg.type = 'button';
819
- weg.setAttribute('aria-label', 'Bundle ' + b.name + ' aus ' + zielTitel(z) + ' entfernen');
820
- weg.addEventListener('click', function () { frageBundleEntfernen(b, z.ziel); });
821
- kopf.appendChild(weg);
883
+ weg.setAttribute('aria-label', 'Bundle ' + b.name + ' entfernen');
884
+ weg.addEventListener('click', entfernen);
885
+ aktionen.appendChild(weg);
886
+ var offen = !!BUNDLE_OFFEN[schluessel];
887
+ var koerperId = 'bundlekoerper-' + bundleKoerperNr++;
888
+ var umschalter = e('button', 'aufklappknopf');
889
+ umschalter.type = 'button';
890
+ umschalter.setAttribute('aria-expanded', offen ? 'true' : 'false');
891
+ umschalter.setAttribute('aria-controls', koerperId);
892
+ umschalter.setAttribute('aria-label', 'Skills des Bundles ' + b.name + ' ein- oder ausklappen');
893
+ var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
894
+ pfeil.setAttribute('viewBox', '0 0 16 16');
895
+ pfeil.setAttribute('class', 'akkordeonpfeil dunkel');
896
+ pfeil.setAttribute('aria-hidden', 'true');
897
+ pfeil.innerHTML = '<path d="M4 6.5 8 10.5 12 6.5"/>';
898
+ umschalter.appendChild(pfeil);
899
+ aktionen.appendChild(umschalter);
900
+ kopf.appendChild(aktionen);
822
901
  gruppe.appendChild(kopf);
823
902
  var ul = e('ul', 'linkliste');
824
- eintraege.forEach(function (ei) { ul.appendChild(linkZeile(ei, z, b)); });
903
+ ul.id = koerperId;
904
+ ul.hidden = !offen;
905
+ zeilen.forEach(function (li) { ul.appendChild(li); });
825
906
  gruppe.appendChild(ul);
907
+ function schalte() {
908
+ offen = !offen;
909
+ BUNDLE_OFFEN[schluessel] = offen;
910
+ umschalter.setAttribute('aria-expanded', offen ? 'true' : 'false');
911
+ ul.hidden = !offen;
912
+ }
913
+ umschalter.addEventListener('click', schalte);
914
+ // Der Kopf schaltet mit, außer über seine Knöpfe (wie der Kopf einer Ziel-Karte).
915
+ kopf.addEventListener('click', function (ev) {
916
+ if (ev.target.closest('button, a') === null) schalte();
917
+ });
826
918
  return gruppe;
827
919
  }
828
920
 
829
- /** Ein Bundle als einzelner Streifen (Chip „Alle" einer Verzeichnis-Karte, E69/E53/E95): die
830
- Harness-Zeichen aller Harnesses, in denen es liegt (wie in den Skill-Zeilen), Name, Mitglieder,
831
- Entfernen für alle diese Harnesses. `treffer`: [{b, z}], je Harness einer, gleicher Name und
832
- gleiche Quelle. Mitglieder: die des ersten Treffers, abweichende anderer Harnesses folgen. */
833
- function bundleStreifen(treffer) {
834
- var b = treffer[0].b;
835
- var streifen = e('div', 'bundlegruppe');
836
- var kopf = e('div', 'bundlekopf');
837
- var titel = e('span', 'bundletitel');
838
- var harnesse = [];
839
- treffer.forEach(function (t) { if (harnesse.indexOf(t.z.ziel.harness) === -1) harnesse.push(t.z.ziel.harness); });
840
- harnesse.sort(function (x, y) { return harnessName(x).localeCompare(harnessName(y), 'de'); });
841
- titel.appendChild(harnesseZeichenSammel(harnesse));
842
- titel.appendChild(e('span', 'etikett', 'Bundle'));
843
- titel.appendChild(e('b', 'bundlename', b.name));
844
- kopf.appendChild(titel);
845
- var mitglieder = [];
846
- treffer.forEach(function (t) {
847
- t.b.skills.forEach(function (n) { if (mitglieder.indexOf(n) === -1) mitglieder.push(n); });
848
- });
849
- kopf.appendChild(e('span', 'bundlemitglieder', mitglieder.join(', ')));
850
- var weg = e('button', 'knopf leise klein', 'Entfernen');
851
- weg.type = 'button';
852
- weg.setAttribute('aria-label', 'Bundle ' + b.name + ' aus '
853
- + treffer.map(function (t) { return zielTitel(t.z); }).join(', ') + ' entfernen');
854
- weg.addEventListener('click', function () {
855
- if (treffer.length === 1) frageBundleEntfernen(b, treffer[0].z.ziel);
856
- else frageBundleEntfernenAlle(b.name, mitglieder, treffer);
921
+ /** Zeile eines nicht verlinkten Mitglieds (E114 L3): blass, „nicht verlinkt", „Wieder verlinken".
922
+ `skill` aus dem Katalog oder null, wenn die Definition oder Quelle nicht lesbar ist. */
923
+ function nichtVerlinktZeile(name, skill, wiederVerlinken) {
924
+ var li = e('li', 'nichtverlinkt');
925
+ li.appendChild(e('span'));
926
+ li.appendChild(e('span', 'name', name));
927
+ if (skill) li.appendChild(wasZelle(skill));
928
+ else {
929
+ var feld = e('span', 'wasfeld');
930
+ feld.appendChild(e('span', 'was fehlt', 'Beschreibung nicht lesbar, die Quelle fehlt hier'));
931
+ li.appendChild(feld);
932
+ }
933
+ li.appendChild(e('span', 'quelle', 'nicht verlinkt'));
934
+ var knopf = e('button', 'knopf leise klein', 'Wieder verlinken');
935
+ knopf.type = 'button';
936
+ knopf.setAttribute('aria-label', name + ' wieder verlinken');
937
+ knopf.addEventListener('click', wiederVerlinken);
938
+ li.appendChild(knopf);
939
+ return li;
940
+ }
941
+
942
+ /** Ein installiertes Bundle unter dem Harness-Chip (E69, E114): alle Mitglieder in der Gruppe,
943
+ verlinkte als gewöhnliche Link-Zeilen, nicht verlinkte blass mit „Wieder verlinken". */
944
+ function bundleGruppe(b, z, eintraege) {
945
+ var zeilen = eintraege.map(function (ei) { return linkZeile(ei, z, b); });
946
+ (b.nichtVerlinkt || []).slice().sort(function (x, y) { return x.localeCompare(y, 'de'); }).forEach(function (name) {
947
+ zeilen.push(nichtVerlinktZeile(name, bundleSkillNachName(b, name), function () {
948
+ bundleAufnehmen(b, [z.ziel], [name]);
949
+ }));
857
950
  });
858
- kopf.appendChild(weg);
859
- streifen.appendChild(kopf);
860
- return streifen;
951
+ return bundleRahmen(b, null, b.skills.length, bundleAlleMitglieder(b).length, [z.ziel], zeilen, function () {
952
+ frageBundleEntfernen(b, z.ziel);
953
+ }, 'ziel\u0000' + zielKey(z.ziel) + '\u0000' + b.name + '\u0000' + b.quelleText);
861
954
  }
862
955
 
863
956
  function linkTeil(z) {
@@ -881,8 +974,13 @@
881
974
  }
882
975
  }
883
976
 
977
+ // Bundles zuerst (E69): sie stehen als Gruppe über den Einzel-Zeilen, ihre Mitglieder liegen
978
+ // in der Gruppe statt in der allgemeinen Liste. Was zum Bundle gehört, sagt der Kern. Eine Gruppe
979
+ // steht auch, wenn kein Mitglied mehr verlinkt ist („0 von N", E114).
980
+ var bundles = (z.bundles || []).filter(function (b) { return bundleAlleMitglieder(b).length > 0; });
981
+
884
982
  var fussContainer;
885
- if (eigene.length === 0) {
983
+ if (eigene.length === 0 && bundles.length === 0) {
886
984
  var leer = e('div', 'leerzeile');
887
985
  leer.appendChild(e('p', 'ohne', 'Hier liegt noch nichts.'));
888
986
  teil.appendChild(leer);
@@ -898,9 +996,6 @@
898
996
  kopf.appendChild(e('span'));
899
997
  teil.appendChild(kopf);
900
998
 
901
- // Bundles zuerst (E69): sie stehen als Gruppe über den Einzel-Zeilen, ihre Mitglieder liegen
902
- // in der Gruppe statt in der allgemeinen Liste. Was zum Bundle gehört, sagt der Kern.
903
- var bundles = (z.bundles || []).filter(function (b) { return b.skills.length > 0; });
904
999
  var bundleVon = {};
905
1000
  bundles.forEach(function (b) { b.skills.forEach(function (n) { bundleVon[n] = b; }); });
906
1001
  bundles
@@ -931,7 +1026,7 @@
931
1026
 
932
1027
  // Ohne eigene Links (leer-Fall) steht der MCP-Abschnitt hinter der Leerzeile, die dort schon den
933
1028
  // Kartenfuß bildet (Nachricht + Knöpfe in einer Zeile, wie überall sonst im leeren Zustand).
934
- if (eigene.length === 0) {
1029
+ if (eigene.length === 0 && bundles.length === 0) {
935
1030
  var mcpAbschnOhneTabelle = mcpAbschnitt(z.ziel);
936
1031
  if (mcpAbschnOhneTabelle) teil.appendChild(mcpAbschnOhneTabelle);
937
1032
  }
@@ -1220,7 +1315,37 @@
1220
1315
  verzMcpZiele.push(z);
1221
1316
  });
1222
1317
 
1223
- if (reihenfolge.length === 0) {
1318
+ // Bundles unter „Alle" (E69, E95, E114 L1): eine aufklappbare Gruppe je Bundle, wie unter dem
1319
+ // Harness-Chip, aber über alle Harnesses am Ort zusammengefasst: Schlüssel ist Name plus Quelle,
1320
+ // gleichnamige Bundles aus zwei Quellen bleiben getrennt. Eine Gruppe steht auch, wenn kein
1321
+ // Mitglied mehr verlinkt ist („0 von N").
1322
+ var bundleGruppen = {}, bundleReihenfolge = [];
1323
+ teile.forEach(function (z) {
1324
+ (z.bundles || [])
1325
+ .filter(function (b) { return bundleAlleMitglieder(b).length > 0; })
1326
+ .forEach(function (b) {
1327
+ var schluessel = b.name + '\u0000' + b.quelleText;
1328
+ if (!bundleGruppen[schluessel]) { bundleGruppen[schluessel] = []; bundleReihenfolge.push(schluessel); }
1329
+ bundleGruppen[schluessel].push({ b: b, z: z });
1330
+ });
1331
+ });
1332
+ // Eine Tabellenzeile geht in einer Gruppe auf, wenn ihr Name in JEDEM Harness der Zeile ein
1333
+ // verlinktes Mitglied dieses Bundles ist — dann steht sie nur in der Gruppe, nicht zusätzlich in
1334
+ // der Tabelle (E114 L1). Hängt derselbe Skill in einem Harness als Einzel-Link, bleibt die
1335
+ // Zeile in der Tabelle. Die Zählung im Kartenkopf (E71) bleibt davon unberührt.
1336
+ var zeileInGruppe = {};
1337
+ reihenfolge.forEach(function (key) {
1338
+ var row = zeilen[key];
1339
+ bundleReihenfolge.forEach(function (gk) {
1340
+ if (zeileInGruppe[key]) return;
1341
+ var ganz = row.teile.every(function (z) {
1342
+ return bundleGruppen[gk].some(function (t) { return t.z === z && t.b.skills.indexOf(row.name) > -1; });
1343
+ });
1344
+ if (ganz) zeileInGruppe[key] = gk;
1345
+ });
1346
+ });
1347
+
1348
+ if (reihenfolge.length === 0 && bundleReihenfolge.length === 0) {
1224
1349
  var leer = e('div', 'leerzeile');
1225
1350
  leer.appendChild(e('p', 'ohne', 'Hier liegt noch nichts.'));
1226
1351
  leer.appendChild(alleSkillsKnopf(pfad, teile));
@@ -1236,27 +1361,6 @@
1236
1361
  return teil;
1237
1362
  }
1238
1363
 
1239
- // Bundles unter „Alle" (E69): die Zeilen bleiben nach Name und Quelle gebündelt (E53), ein
1240
- // Bundle gehört aber zu genau einem Harness — deshalb hier ein Streifen je Bundle mit seinem
1241
- // Harness-Zeichen statt einer Umgruppierung der Tabelle. Unter dem Harness-Chip zeigt linkTeil
1242
- // die volle Gruppe über den Zeilen.
1243
- // Ein Bundle, das in mehreren Harnesses am selben Ort liegt, ist hier ein Streifen (E95), wie
1244
- // die Skill-Zeilen: Schlüssel ist Name plus Quelle, gleichnamige Bundles aus zwei Quellen
1245
- // bleiben getrennt.
1246
- var bundleGruppen = {}, bundleReihenfolge = [];
1247
- teile.forEach(function (z) {
1248
- (z.bundles || [])
1249
- .filter(function (b) { return b.skills.length > 0; })
1250
- .forEach(function (b) {
1251
- var schluessel = b.name + '\u0000' + b.quelleText;
1252
- if (!bundleGruppen[schluessel]) { bundleGruppen[schluessel] = []; bundleReihenfolge.push(schluessel); }
1253
- bundleGruppen[schluessel].push({ b: b, z: z });
1254
- });
1255
- });
1256
- bundleReihenfolge
1257
- .sort(function (a, b) { return bundleGruppen[a][0].b.name.localeCompare(bundleGruppen[b][0].b.name, 'de'); })
1258
- .forEach(function (k) { teil.appendChild(bundleStreifen(bundleGruppen[k])); });
1259
-
1260
1364
  var kopf = e('div', 'linkkopf');
1261
1365
  kopf.appendChild(e('span'));
1262
1366
  kopf.appendChild(e('span', 'etikett', 'Skill'));
@@ -1265,16 +1369,17 @@
1265
1369
  kopf.appendChild(e('span'));
1266
1370
  teil.appendChild(kopf);
1267
1371
 
1268
- var ul = e('ul', 'linkliste');
1269
- reihenfolge
1372
+ var sortiert = reihenfolge
1270
1373
  .slice()
1271
1374
  .sort(function (ka, kb) {
1272
1375
  var a = zeilen[ka], b = zeilen[kb];
1273
1376
  if (a.defekt !== b.defekt) return a.defekt ? -1 : 1;
1274
1377
  return a.name.localeCompare(b.name, 'de');
1275
- })
1276
- .forEach(function (key) {
1277
- var row = zeilen[key];
1378
+ });
1379
+
1380
+ /** Eine gebündelte Zeile unter „Alle" (E53). `bundleName` gesetzt: sie steht in einer
1381
+ Bundle-Gruppe, das Entfernen nimmt den Skill dort heraus (E114). */
1382
+ function alleZeile(row, bundleName) {
1278
1383
  var harnesse = [];
1279
1384
  row.teile.forEach(function (z) { if (harnesse.indexOf(z.ziel.harness) === -1) harnesse.push(z.ziel.harness); });
1280
1385
  harnesse.sort(function (a, b) { return harnessName(a).localeCompare(harnessName(b), 'de'); });
@@ -1325,14 +1430,64 @@
1325
1430
  // Namens nebeneinander, sobald ein Skill je Harness aus verschiedener Quelle hängt.
1326
1431
  weg.setAttribute('aria-label', 'Link auf ' + row.name + ' in '
1327
1432
  + entfernbar.map(function (z) { return harnessName(z.ziel.harness); }).join(', ') + ' entfernen');
1328
- weg.addEventListener('click', function () { frageEntfernenAlle(row.name, pfad, entfernbar, nichtEntfernbar); });
1433
+ weg.addEventListener('click', function () {
1434
+ frageEntfernenAlle(row.name, pfad, entfernbar, nichtEntfernbar, bundleName);
1435
+ });
1329
1436
  li.appendChild(weg);
1330
1437
  } else {
1331
1438
  li.appendChild(e('span', 'kein-weg', 'nur über Plugin „' + pluginNamen.join('“, „') + '“'));
1332
1439
  }
1333
- ul.appendChild(li);
1440
+ return li;
1441
+ }
1442
+
1443
+ // Erst die Bundle-Gruppen, darunter die übrigen Zeilen (wie unter dem Harness-Chip, E69).
1444
+ bundleReihenfolge
1445
+ .sort(function (a, b) { return bundleGruppen[a][0].b.name.localeCompare(bundleGruppen[b][0].b.name, 'de'); })
1446
+ .forEach(function (gk) {
1447
+ var treffer = bundleGruppen[gk];
1448
+ var b = treffer[0].b;
1449
+ var harnesse = [];
1450
+ treffer.forEach(function (t) { if (harnesse.indexOf(t.z.ziel.harness) === -1) harnesse.push(t.z.ziel.harness); });
1451
+ harnesse.sort(function (x, y) { return harnessName(x).localeCompare(harnessName(y), 'de'); });
1452
+ // Mitglieder über alle Harnesses ohne Doppelte; verlinkt zählt, was in jedem Harness liegt.
1453
+ var mitglieder = [];
1454
+ treffer.forEach(function (t) {
1455
+ bundleAlleMitglieder(t.b).forEach(function (n) { if (mitglieder.indexOf(n) === -1) mitglieder.push(n); });
1456
+ });
1457
+ var ueberall = mitglieder.filter(function (n) {
1458
+ return treffer.every(function (t) { return t.b.skills.indexOf(n) > -1; });
1459
+ });
1460
+ var gruppenZeilen = sortiert
1461
+ .filter(function (key) { return zeileInGruppe[key] === gk; })
1462
+ .map(function (key) { return alleZeile(zeilen[key], b.name); });
1463
+ // Nirgends verlinkte Mitglieder als blasse Zeile; „Wieder verlinken" in jedem Harness, in
1464
+ // dem das Mitglied fehlt.
1465
+ mitglieder
1466
+ .filter(function (n) { return treffer.every(function (t) { return t.b.skills.indexOf(n) === -1; }); })
1467
+ .sort(function (x, y) { return x.localeCompare(y, 'de'); })
1468
+ .forEach(function (n) {
1469
+ gruppenZeilen.push(nichtVerlinktZeile(n, bundleSkillNachName(b, n), function () {
1470
+ bundleAufnehmen(b, treffer
1471
+ .filter(function (t) { return (t.b.nichtVerlinkt || []).indexOf(n) > -1; })
1472
+ .map(function (t) { return t.z.ziel; }), [n]);
1473
+ }));
1474
+ });
1475
+ var verlinkteNamen = [];
1476
+ treffer.forEach(function (t) {
1477
+ t.b.skills.forEach(function (n) { if (verlinkteNamen.indexOf(n) === -1) verlinkteNamen.push(n); });
1478
+ });
1479
+ teil.appendChild(bundleRahmen(b, harnesse, ueberall.length, mitglieder.length,
1480
+ treffer.map(function (t) { return t.z.ziel; }), gruppenZeilen, function () {
1481
+ if (treffer.length === 1) frageBundleEntfernen(b, treffer[0].z.ziel);
1482
+ else frageBundleEntfernenAlle(b.name, verlinkteNamen, treffer);
1483
+ }, 'verz\u0000' + pfad + '\u0000' + gk));
1334
1484
  });
1335
- teil.appendChild(ul);
1485
+
1486
+ var ul = e('ul', 'linkliste');
1487
+ sortiert
1488
+ .filter(function (key) { return !zeileInGruppe[key]; })
1489
+ .forEach(function (key) { ul.appendChild(alleZeile(zeilen[key], null)); });
1490
+ if (ul.childNodes.length > 0) teil.appendChild(ul);
1336
1491
 
1337
1492
  // MCP-Abschnitt (E61-GUI-Nachtrag): auf Kartenebene unter „Alle", wie unter dem Harness-Chip,
1338
1493
  // der an diesem Pfad MCP kann.
@@ -1638,28 +1793,42 @@
1638
1793
  // entfernenKontext.eintraege: [{harness, ort}], immer eines bei frageEntfernen (Einzel-Ziel),
1639
1794
  // mehrere bei frageEntfernenAlle (E53 Pkt. 2, „Alle"-Zeile über mehrere Harnesses).
1640
1795
  var entfernenKontext = null;
1641
- /** `bundleName` gesetzt heißt: der Link gehört zu einem Bundle, das Entfernen zerbricht es (E69). */
1796
+ /** Satz für den Entfernen-Dialog einer Zeile in einer Bundle-Gruppe (E114 L2): das Bundle bleibt. */
1797
+ function herausnehmenSatz(bundleName) {
1798
+ return ' Der Skill wird damit aus dem Bundle „' + bundleName + '“ herausgenommen: das Bundle bleibt, '
1799
+ + 'das Aktualisieren legt den Link nicht wieder an. „Wieder verlinken“ oder „Vervollständigen“ am Bundle holt ihn zurück.';
1800
+ }
1801
+
1802
+ /** `bundleName` gesetzt heißt: der Link gehört zu einem Bundle, das Entfernen nimmt ihn dort
1803
+ heraus (E114, früher Zerbrechen nach E69). */
1642
1804
  function frageEntfernen(name, ziel, bundleName) {
1643
1805
  entfernenKontext = { art: 'link', name: name, eintraege: [{ harness: ziel.harness, ort: ziel.ort }] };
1644
1806
  $('dx-titel').textContent = 'Link auf ' + name + ' entfernen?';
1645
1807
  var text = 'Der Link in ' + zielTitel({ ziel: ziel }) + ' verschwindet. Der Checkout bleibt liegen, bis kein Link mehr darauf zeigt.';
1646
- if (bundleName) {
1647
- text += ' Damit zerbricht das Bundle „' + bundleName + '“ hier: seine übrigen Links bleiben liegen, aber nicht mehr als Bundle.';
1648
- }
1808
+ if (bundleName) text += herausnehmenSatz(bundleName);
1649
1809
  $('dx-text').textContent = text;
1650
1810
  zeige($('d-entfernen'));
1651
1811
  }
1652
1812
 
1653
- /** Entfernen des ganzen Bundles an einem Ziel (E69): alle seine Links auf einmal. */
1813
+ /** Zusatz für die Bundle-Dialoge (E114): was schon nicht mehr verlinkt ist, bleibt unberührt. */
1814
+ function nichtVerlinktSatz(namen) {
1815
+ return namen.length === 0 ? '' : ' Nicht verlinkt sind schon: ' + namen.join(', ') + '.';
1816
+ }
1817
+
1818
+ /** Entfernen des ganzen Bundles an einem Ziel (E69, E114 L2): alle noch verlinkten Mitglieder auf
1819
+ einmal, auch nach Teil-Löschung. */
1654
1820
  function frageBundleEntfernen(b, ziel) {
1655
1821
  entfernenKontext = { art: 'bundle', bundle: b.name, ziele: [ziel] };
1656
1822
  $('dx-titel').textContent = 'Bundle ' + b.name + ' entfernen?';
1657
- $('dx-text').textContent = zahlwort(b.skills.length, 'Link', 'Links') + ' aus ' + zielTitel({ ziel: ziel })
1658
- + ' verschwinden: ' + b.skills.join(', ') + '. Die Checkouts bleiben liegen, bis kein Link mehr darauf zeigt.';
1823
+ $('dx-text').textContent = (b.skills.length === 0
1824
+ ? 'Kein Link mehr da, aus ' + zielTitel({ ziel: ziel }) + ' verschwindet nur der Bundle-Verweis.'
1825
+ : zahlwort(b.skills.length, 'Link', 'Links') + ' aus ' + zielTitel({ ziel: ziel })
1826
+ + ' verschwinden: ' + b.skills.join(', ') + '. Die Checkouts bleiben liegen, bis kein Link mehr darauf zeigt.')
1827
+ + nichtVerlinktSatz(b.nichtVerlinkt || []);
1659
1828
  zeige($('d-entfernen'));
1660
1829
  }
1661
- /** Entfernen eines Bundles in allen Harnesses seines Streifens (E95): `treffer` wie bei
1662
- bundleStreifen, der Dialog nennt die Harnesses mit ihrer Linkzahl und die Mitglieder. */
1830
+ /** Entfernen eines Bundles in allen Harnesses seiner Gruppe unter „Alle" (E95): `treffer` je
1831
+ Harness {b, z}, der Dialog nennt die Harnesses mit ihrer Linkzahl und die verlinkten Mitglieder. */
1663
1832
  function frageBundleEntfernenAlle(name, mitglieder, treffer) {
1664
1833
  entfernenKontext = { art: 'bundle', bundle: name, ziele: treffer.map(function (t) { return t.z.ziel; }) };
1665
1834
  var proHarness = treffer.map(function (t) {
@@ -1667,13 +1836,15 @@
1667
1836
  });
1668
1837
  $('dx-titel').textContent = 'Bundle ' + name + ' entfernen?';
1669
1838
  $('dx-text').textContent = 'Das Bundle verschwindet in ' + treffer.length + ' Harnesses (' + proHarness.join(', ')
1670
- + '): ' + mitglieder.join(', ') + '. Die Checkouts bleiben liegen, bis kein Link mehr darauf zeigt.';
1839
+ + ')' + (mitglieder.length === 0 ? '.' : ': ' + mitglieder.join(', ') + '.')
1840
+ + ' Die Checkouts bleiben liegen, bis kein Link mehr darauf zeigt.';
1671
1841
  zeige($('d-entfernen'));
1672
1842
  }
1673
1843
  /** Entfernen einer „Alle"-Zeile (E53 Pkt. 2): entfernbareZiele sind die Ziele (voller z.ziel), in
1674
1844
  denen der Link weg soll, nichtEntfernbareHarnesse die, in denen er nur über ein Plugin hängt
1675
- und liegen bleibt. Der Dialog nennt beides. */
1676
- function frageEntfernenAlle(name, pfad, entfernbareZiele, nichtEntfernbareHarnesse) {
1845
+ und liegen bleibt. Der Dialog nennt beides. `bundleName` gesetzt: die Zeile steht in einer
1846
+ Bundle-Gruppe, der Skill wird dort herausgenommen (E114). */
1847
+ function frageEntfernenAlle(name, pfad, entfernbareZiele, nichtEntfernbareHarnesse, bundleName) {
1677
1848
  entfernenKontext = {
1678
1849
  art: 'link',
1679
1850
  name: name,
@@ -1686,6 +1857,7 @@
1686
1857
  text += ' In ' + nichtEntfernbareHarnesse.map(harnessName).join(', ') + ' bleibt er bestehen (nur über das Plugin).';
1687
1858
  }
1688
1859
  text += ' Der Checkout bleibt liegen, bis kein Link mehr darauf zeigt.';
1860
+ if (bundleName) text += herausnehmenSatz(bundleName);
1689
1861
  $('dx-text').textContent = text;
1690
1862
  zeige($('d-entfernen'));
1691
1863
  }
@@ -2328,13 +2500,14 @@
2328
2500
  var txt = e('span');
2329
2501
  txt.appendChild(e('b', null, bundle.name));
2330
2502
  txt.appendChild(e('span', 'notiz', bundle.beschreibung));
2331
- var herkunft = e('span', 'mono', zahlwort(bundle.mitglieder, 'Skill', 'Skills')
2503
+ var herkunft = e('span', 'mono', zahlwort(bundle.mitglieder.length, 'Skill', 'Skills')
2332
2504
  + ' aus ' + zahlwort(bundle.quellen.length, 'Quelle', 'Quellen') + ' · ' + bundle.quelleName);
2333
2505
  herkunft.title = bundle.quellen.join('\n');
2334
2506
  txt.appendChild(herkunft);
2335
2507
  if (ueberall) txt.appendChild(e('span', 'notiz', zieleZahl > 1 ? 'Hängt schon überall.' : 'Hängt hier schon.'));
2336
2508
  else if (schon > 0) txt.appendChild(e('span', 'notiz', 'Hängt schon in ' + schon + ' von ' + zieleZahl + ' Harnesses.'));
2337
2509
  label.appendChild(txt);
2510
+ bundleAufklappbereich(bundle, label, txt, i);
2338
2511
  box.appendChild(label);
2339
2512
  });
2340
2513
  $('dsk-bundlebox').hidden = BUNDLES.length === 0 && BUNDLES_KAPUTT.length === 0;
@@ -2346,6 +2519,54 @@
2346
2519
  }).join('\n');
2347
2520
  }
2348
2521
 
2522
+ /** Aufklappbereich einer Bundle-Wahlzeile (E114 L1, F133=O3): die Skills des Bundles mit ihrer
2523
+ Beschreibung, vor der Installation. Muster und Verhalten wie skillAufklappbereich (E92): Knopf
2524
+ oben rechts am Label, Körper als eigenes Grid-Kind, preventDefault gegen das Umschalten der
2525
+ Checkbox, je Dialog-Öffnung zugeklappt. Name und Beschreibung kommen aus dem Skill-Katalog;
2526
+ liegt ein Mitglied in einer noch nicht eingerichteten Quelle, steht dort sein Pfad. */
2527
+ function bundleAufklappbereich(bundle, label, txt, index) {
2528
+ var koerperId = 'dsk-bundlekoerper-' + index;
2529
+ var knopf = e('button', 'aufklappknopf');
2530
+ knopf.type = 'button';
2531
+ knopf.setAttribute('aria-expanded', 'false');
2532
+ knopf.setAttribute('aria-controls', koerperId);
2533
+ knopf.setAttribute('aria-label', 'Skills des Bundles ' + bundle.name + ' ein- oder ausklappen');
2534
+ var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2535
+ pfeil.setAttribute('viewBox', '0 0 16 16');
2536
+ pfeil.setAttribute('class', 'akkordeonpfeil dunkel');
2537
+ pfeil.setAttribute('aria-hidden', 'true');
2538
+ pfeil.innerHTML = '<path d="M4 6.5 8 10.5 12 6.5"/>';
2539
+ knopf.appendChild(pfeil);
2540
+
2541
+ var koerper = e('div', 'wahlkoerper');
2542
+ koerper.id = koerperId;
2543
+ koerper.hidden = true;
2544
+ bundle.mitglieder.forEach(function (m) {
2545
+ var skill = bundleMitgliedSkill(m);
2546
+ var zeile = e('p', 'skilldetail bundlemitglied');
2547
+ if (skill) {
2548
+ zeile.appendChild(e('b', null, skill.name + ': '));
2549
+ zeile.appendChild(document.createTextNode(skill.beschreibung || 'keine Beschreibung in der SKILL.md'));
2550
+ } else {
2551
+ zeile.appendChild(e('b', null, m.pfad + ': '));
2552
+ zeile.appendChild(document.createTextNode('aus ' + m.quelleText + ', die Quelle ist hier noch nicht eingerichtet'));
2553
+ }
2554
+ koerper.appendChild(zeile);
2555
+ });
2556
+
2557
+ knopf.addEventListener('click', function (ev) {
2558
+ ev.preventDefault();
2559
+ var war = knopf.getAttribute('aria-expanded') === 'true';
2560
+ knopf.setAttribute('aria-expanded', war ? 'false' : 'true');
2561
+ koerper.hidden = war;
2562
+ });
2563
+ koerper.addEventListener('click', function (ev) { ev.preventDefault(); });
2564
+
2565
+ txt.classList.add('mitknopf');
2566
+ label.appendChild(knopf);
2567
+ label.appendChild(koerper);
2568
+ }
2569
+
2349
2570
  /** Wie oft ein Bundle in den angesteuerten Zielen schon installiert ist (Name plus Quelle, E69). */
2350
2571
  function bundleHaengtIn(teile, bundle) {
2351
2572
  return teile.filter(function (z) {
@@ -3356,7 +3577,12 @@
3356
3577
  var w = $('mcpliste');
3357
3578
  w.textContent = '';
3358
3579
  if (MCP_KATALOG.length === 0) w.appendChild(mcpEinladung());
3359
- else MCP_KATALOG.forEach(function (k) { w.appendChild(mcpKarte(k)); });
3580
+ else {
3581
+ // E113: Kopie nach Name, MCP_KATALOG selbst bleibt in Lieferreihenfolge (F105).
3582
+ MCP_KATALOG.slice().sort(function (a, b) {
3583
+ return a.eintrag.name.localeCompare(b.eintrag.name, 'de', { sensitivity: 'base' });
3584
+ }).forEach(function (k) { w.appendChild(mcpKarte(k)); });
3585
+ }
3360
3586
  $('navzahlmcp').textContent = MCP_KATALOG.length;
3361
3587
  }
3362
3588
 
@@ -3388,6 +3614,12 @@
3388
3614
  // Die Plätze kommen aus dem Kern (F42, E16): die Fähigkeit hängt am Adapter, nicht am
3389
3615
  // Harness-Namen, hier wird nicht selbst entschieden, wer welche Ebene kennt.
3390
3616
  var plaetze = await rufe(function () { return window.hmgr.mcpPlaetze(); });
3617
+ // E113: nach angezeigtem Titel (Harness · Ort), bei Gleichstand nach der Dateispur. Die Kopie
3618
+ // ist zugleich die Auswahlliste, damit der Radio-Index zur sortierten Reihenfolge passt.
3619
+ plaetze = plaetze.slice().sort(function (a, b) {
3620
+ return zielTitel({ ziel: a }).localeCompare(zielTitel({ ziel: b }), 'de', { sensitivity: 'base' })
3621
+ || mcpOrtSpur(a).localeCompare(mcpOrtSpur(b), 'de', { sensitivity: 'base' });
3622
+ });
3391
3623
  mcpPlaetzeAuswahl = plaetze;
3392
3624
  plaetze.forEach(function (ziel, i) {
3393
3625
  var schon = k.eingehaengt.some(function (z) { return zielKey(z) === zielKey(ziel); });
@@ -3522,7 +3754,13 @@
3522
3754
  box.appendChild(e('p', 'ohne', 'Noch kein Server im Katalog. hmgr liest ihn aus '
3523
3755
  + pfadSchreibweise('~/.hmgr/mcp.yaml') + ' und schreibt die Datei nie.'));
3524
3756
  }
3525
- MCP_KATALOG.forEach(function (k, i) {
3757
+ // E113: nach Name sortiert. Der Wert der Checkbox bleibt der Index in MCP_KATALOG (Lieferreihenfolge),
3758
+ // den die Auswertung beim Absenden dort nachschlägt.
3759
+ MCP_KATALOG.map(function (k, i) { return { k: k, i: i }; }).sort(function (a, b) {
3760
+ return a.k.eintrag.name.localeCompare(b.k.eintrag.name, 'de', { sensitivity: 'base' });
3761
+ }).forEach(function (p) {
3762
+ var k = p.k;
3763
+ var i = p.i;
3526
3764
  var schon = k.eingehaengt.some(function (z) { return zielKey(z) === zielKey(ziel); });
3527
3765
  var label = e('label', 'wahlzeile');
3528
3766
  var input = document.createElement('input');
@@ -3648,7 +3886,12 @@
3648
3886
  var eintrag = null;
3649
3887
  HARNESS_HEIME.forEach(function (x) { if (x.harness === h) eintrag = x; });
3650
3888
  if (!eintrag) return kasten;
3651
- eintrag.heime.forEach(function (heim) {
3889
+ // E113: das primäre Homedir (ohne Adresse) bleibt oben, die zusätzlichen folgen nach Adresse sortiert.
3890
+ var heimeSortiert = eintrag.heime.filter(function (x) { return !x.adresse; }).concat(
3891
+ eintrag.heime.filter(function (x) { return !!x.adresse; }).sort(function (a, b) {
3892
+ return a.adresse.localeCompare(b.adresse, 'de', { sensitivity: 'base' });
3893
+ }));
3894
+ heimeSortiert.forEach(function (heim) {
3652
3895
  var zeile = e('div', 'heimzeile');
3653
3896
  zeile.appendChild(e('span', 'heimart', heim.adresse ? heim.adresse : 'Homedir'));
3654
3897
  var pfad = e('code', 'heimpfad', pfadKurz(heim.pfad));
@@ -3768,7 +4011,10 @@
3768
4011
  var vorher = document.activeElement;
3769
4012
  var merk = vorher && vorher.dataset && ul.contains(vorher) ? vorher.dataset.harness : null;
3770
4013
  ul.textContent = '';
3771
- HARNESSE_ALLE.forEach(function (h) {
4014
+ // E113: Kopie nach Anzeigename, HARNESSE_ALLE bleibt in Lieferreihenfolge (F105).
4015
+ HARNESSE_ALLE.slice().sort(function (a, b) {
4016
+ return harnessName(a).localeCompare(harnessName(b), 'de', { sensitivity: 'base' });
4017
+ }).forEach(function (h) {
3772
4018
  var li = document.createElement('li');
3773
4019
  var label = e('label', 'harnessschalter');
3774
4020
  var input = document.createElement('input');
@@ -798,6 +798,14 @@ dialog::backdrop{background:rgba(9,45,37,.62)}
798
798
  .bundlemitglieder{font-family:var(--mono);font-size:16px;letter-spacing:-.012em;color:var(--tinte-weich)}
799
799
  .bundlekopf .knopf{margin-left:auto}
800
800
  .bundlegruppe .linkliste li:last-child{border-bottom:1px solid var(--strich-zart)}
801
+ /* E114: Knöpfe und Aufklapp-Umschalter rechts im Kopf; der Kopf ist zugleich Klickfläche. */
802
+ .bundlekopf{cursor:pointer}
803
+ .bundleaktionen{margin-left:auto;display:inline-flex;align-items:center;gap:8px}
804
+ .bundleaktionen .knopf{margin-left:0}
805
+ .bundlegruppe .linkliste[hidden]{display:none}
806
+ /* Nicht verlinktes Mitglied (E114 L3): blass, der Knopf bleibt lesbar. */
807
+ .linkliste li.nichtverlinkt .name,.linkliste li.nichtverlinkt .was{color:var(--tinte-leise)}
808
+ .linkliste li.nichtverlinkt .quelle{color:var(--tinte-leise);font-style:italic}
801
809
 
802
810
  /* Bundle- und Skill-Sektion im Dialog „Skills hinzufügen" (E69): zwei benannte Gruppen unter
803
811
  demselben Suchfeld. */