feat(diagram): Personen-Leiste mit Personen-Linse und Belastungsanzeige (D87)
Je @Person eine Pille mit Anteil an der offenen Pfad-Arbeit (D71-Maß), dazu ein Eintrag für Unzugewiesenes — die Anteile summieren auf 100 %. Ein Tipp filtert das Diagramm auf die Knoten der Person: alles andere faltet, ihre Pakete stehen als ▸ n, die Pillen färben petrol. Die Linse ist rein Ansicht und Sitzungssache — in den Text wird nichts geschrieben, in geteilten Dokumenten ginge der persönliche Filter sonst an alle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1e1a00f794
commit
deec39e82a
@@ -19,6 +19,8 @@ reverse.
|
|||||||
|
|
||||||
## 2026-08-27
|
## 2026-08-27
|
||||||
|
|
||||||
|
- A people bar below the diagram shows each `@person` with their share of the open work on the cheapest path — plus an entry for what is assigned to nobody
|
||||||
|
- Tapping a person filters the diagram to their nodes: everything else folds, their packages stand collapsed as `▸ n`, their pills turn teal — a view-only lens that never writes into the text
|
||||||
- Folding regions in the text editor recorded as an idea in the plan — view-only and local, unrelated to the diagram's fold marks, deliberately not sized yet
|
- Folding regions in the text editor recorded as an idea in the plan — view-only and local, unrelated to the diagram's fold marks, deliberately not sized yet
|
||||||
- Fix: the root node's fold chip was white-on-pastel and barely visible — it now looks like every other node's, and the description mark and id line on a coloured root are readable too
|
- Fix: the root node's fold chip was white-on-pastel and barely visible — it now looks like every other node's, and the description mark and id line on a coloured root are readable too
|
||||||
- The caret on a node off the cheapest path now highlights only the ring — the fill stays pale instead of jumping back to full colour
|
- The caret on a node off the cheapest path now highlights only the ring — the fill stays pale instead of jumping back to full colour
|
||||||
|
|||||||
@@ -7446,3 +7446,82 @@ lokales Dokument zeigt unverändert seine lokalen Stände. Der Namens-Dialog
|
|||||||
beim Beitreten blockiert nichts (die Automation wies ihn ab — Dokument lädt,
|
beim Beitreten blockiert nichts (die Automation wies ihn ab — Dokument lädt,
|
||||||
nichts gemerkt, nächstes Mal wird wieder gefragt). 531 Frontend-Tests und
|
nichts gemerkt, nächstes Mal wird wieder gefragt). 531 Frontend-Tests und
|
||||||
der Backend-`check` grün.
|
der Backend-`check` grün.
|
||||||
|
|
||||||
|
## D87 — Personen-Leiste mit Personen-Linse: eine Fläche für Filter und Belastung
|
||||||
|
Gewünscht: je `@`-Person die zuständigen Knoten hervorheben und alles andere
|
||||||
|
zuklappen — auch unter den explizit benannten Knoten —, dazu eine
|
||||||
|
Belastungsanzeige je Person. Zwei Bedienideen standen im Raum (Menü-Knopf im
|
||||||
|
Diagramm-Kopf oder die Namens-Pillen als Leiste unterm Diagramm); die
|
||||||
|
Entscheidungen fielen als Multiple-Choice-Runde:
|
||||||
|
|
||||||
|
**Die Leiste, nicht der Knopf** — und der Ausschlag ist das zweite Feature:
|
||||||
|
Ein Kopf-Knopf wäre das zwölfte Element in einer dreimal zurechtgemessenen
|
||||||
|
Zeile (D47/D56/D75) und zeigte die Belastung erst hinter einem Klick. Die
|
||||||
|
Leiste trägt beides in einer Fläche — Pille, Balken, Tipp = Linse — und
|
||||||
|
erscheint **nur bei Dokumenten mit Tags**: Für tag-lose Pläne (auch den
|
||||||
|
mitgelieferten) kostet sie nichts.
|
||||||
|
|
||||||
|
**Die Linse ist die erste bewusst rein sichtbare Faltung.** D38 schreibt
|
||||||
|
Falten in den Text — richtig für „so wird dieses Dokument eröffnet", falsch
|
||||||
|
für „zeig mir Annas Arbeit": In einem geteilten Dokument (D76) ginge der
|
||||||
|
persönliche Filter als Textänderung an alle Mitschreiber. Die Linse nutzt
|
||||||
|
deshalb nur die Darstellung (Sitzungszustand wie die News-Vorführung, D58);
|
||||||
|
Hand-Faltungen währenddessen bleiben in einer eigenen Überlagerung
|
||||||
|
(`lensOverrides`), der Falt-Durchschalter beendet die Linse (er arbeitet auf
|
||||||
|
dem Text-Zustand), und die Reihum-Prüfung des Durchschalters ruht solange —
|
||||||
|
sonst würfe der Linsen-Zustand die Position um, obwohl der Text unverändert
|
||||||
|
ist. **Export und Druck folgen ihr trotzdem** (Nutzer-Entscheidung): Die
|
||||||
|
D38-Regel „exportiert wird die sichtbare Struktur, eingeklappte Knoten tragen
|
||||||
|
▸ n" gilt wie überall — „Annas Sicht als Bild teilen" funktioniert damit von
|
||||||
|
selbst.
|
||||||
|
|
||||||
|
**Die Falt-Regel (`personFoldSet`, headless in model.js):** Zu klappt, wer
|
||||||
|
den Tag **selbst trägt** (das „▸ n" vertritt das Paket — mit der
|
||||||
|
D71-Vererbung ist alles darunter ohnehin dessen) und wer in seinem Teilbaum
|
||||||
|
**gar nichts** von der Person hat; offen bleiben genau die Vorfahren-Pfade.
|
||||||
|
Der interessante Fall ist getestet: Ein fremd getaggtes Paket, das tief innen
|
||||||
|
einen Knoten der gesuchten Person enthält, bleibt **offen** — die eigenen
|
||||||
|
Tags eines Knotens entscheiden nur über ihn, nicht über den Pfad hindurch.
|
||||||
|
Nicht faltbare Blatt-Geschwister auf offenen Pfaden bleiben sichtbar —
|
||||||
|
dieselbe Eigenschaft wie beim `path`-Preset (D75); welche Knoten gemeint
|
||||||
|
sind, sagt die **petrol gefärbte Pille** der Person (die Engpass-Warnfarbe
|
||||||
|
gewinnt: eine Warnung wiegt schwerer als eine Auswahl).
|
||||||
|
|
||||||
|
**„Ohne Zuständigen" ist ein eigener, klickbarer Eintrag** (gestrichelte
|
||||||
|
Pille — ein Loch, keine Person): Die Anteile summieren damit ehrlich auf
|
||||||
|
100 % (D71: Unzugewiesenes verwässert sie ohnehin), und seine Linse zeigt,
|
||||||
|
wofür noch niemand eingeteilt ist — jeder Knoten mit **eigenen** Tags klappt
|
||||||
|
zu, die Grenze ins zugewiesene Gebiet. Für Unzugewiesen taugt die
|
||||||
|
Personen-Regel nicht: „kein Tag" ist vorfahren-abgeschlossen (die Wurzel
|
||||||
|
wäre ihr eigener äußerster Vertreter), die Grenz-Formulierung ist die
|
||||||
|
tragfähige Umkehrung.
|
||||||
|
|
||||||
|
**Belastung: Anteile, keine absoluten Zahlen** (Nutzer-Entscheidung). Die
|
||||||
|
Größen sind ordinal (D46) — jede Summe ist eine Näherung, und eine Punktzahl
|
||||||
|
suggerierte Präzision, die die Schätzskala nicht hat. Gerechnet wird mit dem
|
||||||
|
vorhandenen D71-Maß (`assigneeLoads`: Marginalkosten der offenen Pfad-Arbeit,
|
||||||
|
Vererbung, geteilte Beiträge) — **eine** Kostenrechnung im System, Warnung
|
||||||
|
und Anzeige sagen dieselbe Zahl. Bei ausgeschaltetem Pfad-Umschalter wird der
|
||||||
|
Pfad eigens gerechnet (die D75-Linie: die Frage gilt dem Pfad, nicht seiner
|
||||||
|
Anzeige); die Bernstein-Färbung der Engpass-Person bleibt dagegen am
|
||||||
|
sichtbaren Pfad und färbt auch die Leisten-Pille.
|
||||||
|
|
||||||
|
**Eine Person zur Zeit** (Nutzer-Entscheidung): Tipp wechselt, zweiter Tipp
|
||||||
|
auf dieselbe schaltet aus — Mehrfachauswahl kann später dazukommen, umgekehrt
|
||||||
|
nicht. Die Linse erlischt von selbst, wenn es die Person (oder überhaupt
|
||||||
|
Tags) im Dokument nicht mehr gibt, und beim Dokumentwechsel
|
||||||
|
(`loadActiveIntoEditor`, die eine Stelle aus D80).
|
||||||
|
|
||||||
|
**Nebengewinn, notiert:** SPEC §11 reserviert `&tag` ausdrücklich bis zum
|
||||||
|
ersten Konsumenten — „Filter-/Hervorheben-Linse" ist dort wörtlich genannt.
|
||||||
|
Diese Linse baut genau die Mechanik; Schlagworte bekämen sie später
|
||||||
|
geschenkt.
|
||||||
|
|
||||||
|
**Nachgemessen** im Browser an einem Tag-Plan (anna/ben, elf Knoten): Leiste
|
||||||
|
ben 50 % · anna 21 % · ohne Zuständigen 29 % (Summe 100). Annas Linse:
|
||||||
|
`Backend` und `Frontend` als „▸ 2", `Infra` offen mit ihrem CI-Blatt, beide
|
||||||
|
anna-Pillen petrol, Text byte-identisch. Bens Linse hält annas Backend
|
||||||
|
**offen** (sein API liegt darin). Hand-Faltung während der Linse ändert den
|
||||||
|
Text nicht; Ausschalten stellt den textdefinierten Zustand wieder her;
|
||||||
|
Unzugewiesen-Linse klappt genau die getaggten Pakete zu. Tag-loses Dokument:
|
||||||
|
keine Leiste. 538 Tests, davon 7 neue in `tests/lens.test.js`.
|
||||||
|
|||||||
@@ -760,6 +760,41 @@ Termine sagt die Warnung nichts (das tut in dieser Notation ohnehin nichts,
|
|||||||
keine Pille und damit keine Färbung. Bei ausgeschaltetem Pfad entfällt
|
keine Pille und damit keine Färbung. Bei ausgeschaltetem Pfad entfällt
|
||||||
beides — ohne Pfad gibt es keine offene Front.
|
beides — ohne Pfad gibt es keine offene Front.
|
||||||
|
|
||||||
|
### Personen-Leiste und Personen-Linse
|
||||||
|
Trägt das Dokument `@`-Tags (§7), steht unter dem Diagramm eine **Leiste mit
|
||||||
|
einer Pille je Person** samt **Belastungs-Balken**: ihr Anteil an der offenen
|
||||||
|
Arbeit des günstigsten Pfads — dasselbe Maß wie der Zuständigen-Engpass
|
||||||
|
(oben), mit Vererbung und geteilten Beiträgen; die Bernstein-Färbung der
|
||||||
|
Engpass-Person erscheint auch hier. Angezeigt werden **Anteile, keine
|
||||||
|
absoluten Zahlen** — die Größen sind ordinal, jede Summe ist eine Näherung.
|
||||||
|
Der Rest **ohne Zuständigen** bekommt einen eigenen, gestrichelten Eintrag,
|
||||||
|
damit die Anteile auf 100 % summieren. Gerechnet wird auch bei
|
||||||
|
ausgeschaltetem Pfad-Umschalter (wie die `path`-Voreinstellung des
|
||||||
|
Falt-Durchschalters: die Frage gilt dem Pfad, nicht seiner Anzeige). Ohne
|
||||||
|
Tags gibt es die Leiste nicht.
|
||||||
|
|
||||||
|
Ein **Tipp auf eine Pille schaltet die Personen-Linse**: Sichtbar bleiben nur
|
||||||
|
die Vorfahren-Pfade zu den Knoten dieser Person; die getaggten Knoten selbst
|
||||||
|
stehen **eingeklappt** da — ihr „▸ n“ vertritt das Paket, und mit der
|
||||||
|
Vererbung ist alles darunter ohnehin ihres. Ihre Personen-Pillen an den
|
||||||
|
sichtbaren Knoten färben sich petrol (die Engpass-Warnfarbe gewinnt). Nicht
|
||||||
|
faltbare Geschwister auf offenen Pfaden bleiben stehen — dieselbe
|
||||||
|
Eigenschaft wie bei der Pfad-Voreinstellung des Falt-Durchschalters. Der
|
||||||
|
Eintrag „ohne Zuständigen“ zeigt als Linse die Struktur, deren Arbeit
|
||||||
|
niemandem gehört: Jeder Knoten mit eigenen Tags klappt zu. Ein zweiter Tipp
|
||||||
|
hebt die Linse auf, ein Tipp auf eine andere Person wechselt; es gilt
|
||||||
|
höchstens eine zugleich.
|
||||||
|
|
||||||
|
Die Linse ist **reine Ansicht und Sitzungssache**: Anders als das Falten (§9,
|
||||||
|
`>`/`<`) schreibt sie **nichts** in den Text — in einem geteilten Dokument
|
||||||
|
(`?live=`) ginge der persönliche Filter sonst als Textänderung an alle.
|
||||||
|
Hand-Faltungen während der Linse bleiben ebenso in der Sitzung;
|
||||||
|
Dokumentwechsel und Neuladen setzen zurück, der Falt-Durchschalter beendet
|
||||||
|
die Linse (er arbeitet auf dem Text-Faltzustand). **Grafikexport und Druck
|
||||||
|
folgen der sichtbaren Faltung** wie überall — „die Sicht einer Person als
|
||||||
|
Bild teilen“ funktioniert damit von selbst; die Leiste selbst erscheint in
|
||||||
|
beiden nicht.
|
||||||
|
|
||||||
### Diagramm aus einer URL laden (`?sourceUrl=`)
|
### Diagramm aus einer URL laden (`?sourceUrl=`)
|
||||||
Der Editor kann den Notationstext aus einer externen Textdatei beziehen:
|
Der Editor kann den Notationstext aus einer externen Textdatei beziehen:
|
||||||
`…?sourceUrl=https://example.org/plan.txt` (relative Angaben werden gegen die
|
`…?sourceUrl=https://example.org/plan.txt` (relative Angaben werden gegen die
|
||||||
|
|||||||
@@ -87,6 +87,9 @@
|
|||||||
- [^] #ed.fold.write: Folding writes the mark back into the text (S) %% one undo step
|
- [^] #ed.fold.write: Folding writes the mark back into the text (S) %% one undo step
|
||||||
- [^] #ed.fold.preset: The fold button cycles through four presets (S) %% M-and-smaller, off-path, all, none — see D44/D75
|
- [^] #ed.fold.preset: The fold button cycles through four presets (S) %% M-and-smaller, off-path, all, none — see D44/D75
|
||||||
+ [?] #ed.textfold: Fold regions in the text editor, next to the line numbers %% view-only, unrelated to the > marks; not sized yet
|
+ [?] #ed.textfold: Fold regions in the text editor, next to the line numbers %% view-only, unrelated to the > marks; not sized yet
|
||||||
|
- [x] #ed.lens: A people bar with a person lens (M)
|
||||||
|
- [x] #ed.lens.fold: One person's nodes, everything else folds (S) %% view-only, never written into the text
|
||||||
|
- [x] #ed.lens.load: A load bar per person, shares of the open path work (S) %% the D71 measure
|
||||||
- [^] #ed.snaps: Earlier states of a document, every ten minutes (S) %% only when something changed
|
- [^] #ed.snaps: Earlier states of a document, every ten minutes (S) %% only when something changed
|
||||||
- [^] #ed.snaps.manual: Save a state by hand, before a larger change (XS) %% ten minutes is the wrong beat for that moment
|
- [^] #ed.snaps.manual: Save a state by hand, before a larger change (XS) %% ten minutes is the wrong beat for that moment
|
||||||
- [^] #ed.files: Open and save .werkbaum files (S)
|
- [^] #ed.files: Open and save .werkbaum files (S)
|
||||||
@@ -619,6 +622,21 @@
|
|||||||
lines, so this hinges on the parked editor-rebuild question (CodeMirror,
|
lines, so this hinges on the parked editor-rebuild question (CodeMirror,
|
||||||
D76); deliberately not sized yet.
|
D76); deliberately not sized yet.
|
||||||
|
|
||||||
|
#ed.lens
|
||||||
|
A bar below the diagram, one pill per @person, shown only when the
|
||||||
|
document carries tags at all. Tapping a pill filters the diagram to that
|
||||||
|
person's work; an extra entry shows what is assigned to nobody.
|
||||||
|
|
||||||
|
#ed.lens.fold
|
||||||
|
The lens keeps only the ancestor paths to the person's nodes open; their
|
||||||
|
tagged packages stand collapsed. Deliberately view-only: written fold
|
||||||
|
marks would push a personal filter to everyone in a shared document.
|
||||||
|
|
||||||
|
#ed.lens.load
|
||||||
|
Each pill carries a bar with the person's share of the open work on the
|
||||||
|
cheapest path — the same measure as the bottleneck warning, shares
|
||||||
|
rather than absolute numbers because the sizes are ordinal.
|
||||||
|
|
||||||
#ed.snaps
|
#ed.snaps
|
||||||
A safety net against mishaps: the text is put aside every ten minutes, but
|
A safety net against mishaps: the text is put aside every ten minutes, but
|
||||||
only when it differs from the last one kept, and the last twenty are offered
|
only when it differs from the last one kept, and the last twenty are offered
|
||||||
|
|||||||
@@ -277,6 +277,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="diagram"><ul class="tree" id="out"></ul></div>
|
<div class="diagram"><ul class="tree" id="out"></ul></div>
|
||||||
|
<div class="peoplebar" id="peopleBar" data-i18n-aria="peopleBarLabel" aria-label="Zuständige" hidden></div>
|
||||||
<div class="warnings" id="warn" role="status" aria-live="polite"></div>
|
<div class="warnings" id="warn" role="status" aria-live="polite"></div>
|
||||||
<div class="zoomctl" data-i18n-aria="zoomAria" aria-label="Zoom (Strg/Cmd + Mausrad)">
|
<div class="zoomctl" data-i18n-aria="zoomAria" aria-label="Zoom (Strg/Cmd + Mausrad)">
|
||||||
<button type="button" id="zoomToggle" class="zoomToggle" data-i18n-title="zoomTooltip" title="Zoom anzeigen" aria-label="Zoom anzeigen">
|
<button type="button" id="zoomToggle" class="zoomToggle" data-i18n-title="zoomTooltip" title="Zoom anzeigen" aria-label="Zoom anzeigen">
|
||||||
|
|||||||
+143
-5
@@ -1,6 +1,6 @@
|
|||||||
import './style.css';
|
import './style.css';
|
||||||
import { parse, setFoldMark, expandShortIds, shortIdClosed } from './parser.js';
|
import { parse, setFoldMark, expandShortIds, shortIdClosed } from './parser.js';
|
||||||
import { computeCheapPlan, overloadedAssignee, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, presetFoldSet, lineTargets } from './model.js';
|
import { computeCheapPlan, overloadedAssignee, assigneeLoads, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, presetFoldSet, personFoldSet, allTags, lineTargets } from './model.js';
|
||||||
import { esc, renderTreeHtml, TIP_RULE } from './render.js';
|
import { esc, renderTreeHtml, TIP_RULE } from './render.js';
|
||||||
import { formatWarning, warningText } from './warnings.js';
|
import { formatWarning, warningText } from './warnings.js';
|
||||||
import * as live from './live.js';
|
import * as live from './live.js';
|
||||||
@@ -52,6 +52,7 @@ const INITIAL = `%% Project structure – Sprint 14
|
|||||||
const src = document.getElementById('src');
|
const src = document.getElementById('src');
|
||||||
const out = document.getElementById('out');
|
const out = document.getElementById('out');
|
||||||
const warnBox = document.getElementById('warn');
|
const warnBox = document.getElementById('warn');
|
||||||
|
const peopleBar = document.getElementById('peopleBar');
|
||||||
/* Zeilennummern-Streifen (D33) — hier oben geholt, weil render() ihn füllt. */
|
/* Zeilennummern-Streifen (D33) — hier oben geholt, weil render() ihn füllt. */
|
||||||
const srcWrap = document.getElementById('srcWrap');
|
const srcWrap = document.getElementById('srcWrap');
|
||||||
const lineNoBox = document.getElementById('lineNos');
|
const lineNoBox = document.getElementById('lineNos');
|
||||||
@@ -130,6 +131,16 @@ let newsDay = null, newsKeySet = null;
|
|||||||
Textmarken, gelten nur für die Sitzung und fallen beim Dokumentwechsel weg.
|
Textmarken, gelten nur für die Sitzung und fallen beim Dokumentwechsel weg.
|
||||||
`foldByLine` ist der Zustand des letzten Renders für Klick/Tastatur. */
|
`foldByLine` ist der Zustand des letzten Renders für Klick/Tastatur. */
|
||||||
let foldOverrides = new Map(), foldByLine = new Map();
|
let foldOverrides = new Map(), foldByLine = new Map();
|
||||||
|
/* Personen-Linse (SPEC §9, D87): `lens` ist null (aus) oder `{tag}` — der
|
||||||
|
Personen-Tag bzw. `tag: null` für „ohne Zuständigen". REIN Ansicht und
|
||||||
|
Sitzungssache: Sie überlagert den Faltzustand nur für die Darstellung,
|
||||||
|
in den Text wird nichts geschrieben — in einem geteilten Dokument ginge
|
||||||
|
der persönliche Filter sonst als Textänderung an alle (D76/D38).
|
||||||
|
`lensOverrides` sind Hand-Faltungen WÄHREND der Linse (Label-Pfad-Schlüssel
|
||||||
|
wie `foldOverrides`); Ausschalten stellt den textdefinierten Zustand
|
||||||
|
wieder her. */
|
||||||
|
let lens = null, lensOverrides = new Map();
|
||||||
|
function clearLens(){ lens = null; lensOverrides.clear(); }
|
||||||
/* Zeile -> Zeile des sichtbaren Vertreters (D38-Nachtrag 4): Für Zeilen in
|
/* Zeile -> Zeile des sichtbaren Vertreters (D38-Nachtrag 4): Für Zeilen in
|
||||||
eingeklappten Teilbäumen zeigt sie auf den nächsten sichtbaren Vorfahren;
|
eingeklappten Teilbäumen zeigt sie auf den nächsten sichtbaren Vorfahren;
|
||||||
`nodeOfLine()` greift darauf zurück, wenn die Zeile keinen DOM-Knoten hat. */
|
`nodeOfLine()` greift darauf zurück, wenn die Zeile keinen DOM-Knoten hat. */
|
||||||
@@ -163,11 +174,17 @@ function render(){
|
|||||||
if(tabWarning) warnings = [tabWarning].concat(warnings); /* fremder Tab schreibt mit (D84) */
|
if(tabWarning) warnings = [tabWarning].concat(warnings); /* fremder Tab schreibt mit (D84) */
|
||||||
if(storeWarning) warnings = [storeWarning].concat(warnings); /* Datenverlust droht — zuoberst (D82) */
|
if(storeWarning) warnings = [storeWarning].concat(warnings); /* Datenverlust droht — zuoberst (D82) */
|
||||||
|
|
||||||
|
/* Personen-Linse (D87): erlischt, sobald es die Person (oder überhaupt
|
||||||
|
Tags) im Dokument nicht mehr gibt — sonst zeigte sie auf nichts. */
|
||||||
|
const docTags = allTags(roots);
|
||||||
|
if(lens && (!docTags.length || (lens.tag !== null && !docTags.includes(lens.tag)))) clearLens();
|
||||||
|
|
||||||
if(!roots.length){
|
if(!roots.length){
|
||||||
out.innerHTML = `<div class="empty">${esc(t('empty'))}</div>`;
|
out.innerHTML = `<div class="empty">${esc(t('empty'))}</div>`;
|
||||||
freshSet = new Set();
|
freshSet = new Set();
|
||||||
foldByLine = new Map();
|
foldByLine = new Map();
|
||||||
lineTargetMap = new Map();
|
lineTargetMap = new Map();
|
||||||
|
peopleBar.hidden = true;
|
||||||
} else {
|
} else {
|
||||||
/* Günstigster Pfad auf der Dependency Closure (D42): scheitert die exakte
|
/* Günstigster Pfad auf der Dependency Closure (D42): scheitert die exakte
|
||||||
Suche an zu vielen gekoppelten Gruppen, wird die gierige Schätzung
|
Suche an zu vielen gekoppelten Gruppen, wird die gierige Schätzung
|
||||||
@@ -215,14 +232,20 @@ function render(){
|
|||||||
er hier eigens für die Prüfung gerechnet (dieselbe Rechnung, die sonst
|
er hier eigens für die Prüfung gerechnet (dieselbe Rechnung, die sonst
|
||||||
ohnehin je Tastendruck läuft). */
|
ohnehin je Tastendruck läuft). */
|
||||||
let presetSet = null, presetMatch = true;
|
let presetSet = null, presetMatch = true;
|
||||||
if(foldCycleApplied >= 0){
|
/* Bei aktiver Linse (D87) ruht die Prüfung: Die Faltung ist dann
|
||||||
|
Linsen-Sache, der Text unverändert — die Reihum-Position bleibt. */
|
||||||
|
if(foldCycleApplied >= 0 && !lens){
|
||||||
const mode = FOLD_CYCLE[foldCycleApplied];
|
const mode = FOLD_CYCLE[foldCycleApplied];
|
||||||
const cs = (mode === 'path' && !cheapPathOn) ? computeCheapPlan(roots).set : cheapSet;
|
const cs = (mode === 'path' && !cheapPathOn) ? computeCheapPlan(roots).set : cheapSet;
|
||||||
presetSet = presetFoldSet(roots, mode, cs);
|
presetSet = presetFoldSet(roots, mode, cs);
|
||||||
}
|
}
|
||||||
|
/* Personen-Linse (D87): Ihr Faltzustand ERSETZT den textdefinierten für
|
||||||
|
die Darstellung — überlagert nur von Hand-Faltungen während der Linse. */
|
||||||
|
const lensSet = lens ? personFoldSet(roots, lens.tag) : null;
|
||||||
keys.forEach((key, n) => {
|
keys.forEach((key, n) => {
|
||||||
const ov = foldOverrides.get(key);
|
const ov = lensSet ? lensOverrides.get(key) : foldOverrides.get(key);
|
||||||
const collapsed = (ov !== undefined ? ov : initFold.has(n)) && n.children.length > 0;
|
const base = lensSet ? lensSet.has(n) : initFold.has(n);
|
||||||
|
const collapsed = (ov !== undefined ? ov : base) && n.children.length > 0;
|
||||||
if(collapsed) collapsedSet.add(n);
|
if(collapsed) collapsedSet.add(n);
|
||||||
foldByLine.set(n.line, {key, collapsed, canFold: n.children.length > 0});
|
foldByLine.set(n.line, {key, collapsed, canFold: n.children.length > 0});
|
||||||
if(presetSet && n.children.length > 0 && collapsed !== presetSet.has(n)) presetMatch = false;
|
if(presetSet && n.children.length > 0 && collapsed !== presetSet.has(n)) presetMatch = false;
|
||||||
@@ -234,9 +257,21 @@ function render(){
|
|||||||
const r = renderTreeHtml(roots, {t, showDiscarded, cheapPath: cheapPathOn, cheapSet, showIds,
|
const r = renderTreeHtml(roots, {t, showDiscarded, cheapPath: cheapPathOn, cheapSet, showIds,
|
||||||
freshSet, collapsedSet,
|
freshSet, collapsedSet,
|
||||||
effStatus: effectiveStatus(roots),
|
effStatus: effectiveStatus(roots),
|
||||||
overloadTag: overload ? overload.tag : null});
|
overloadTag: overload ? overload.tag : null,
|
||||||
|
lensTag: lens ? lens.tag : null});
|
||||||
out.innerHTML = r.html;
|
out.innerHTML = r.html;
|
||||||
warnings = warnings.concat(r.warnings);
|
warnings = warnings.concat(r.warnings);
|
||||||
|
/* Personen-Leiste (D87): Belastung je Person aus der offenen Pfad-Arbeit
|
||||||
|
(dasselbe Maß wie die D71-Warnung). Bei ausgeschaltetem Pfad-Umschalter
|
||||||
|
wird der Pfad eigens gerechnet — die Leiste fragt nach dem Pfad, nicht
|
||||||
|
nach seiner Anzeige (die D75-Linie des 'path'-Presets). Die
|
||||||
|
Bernstein-Färbung des Engpasses bleibt dagegen am SICHTBAREN Pfad. */
|
||||||
|
if(docTags.length){
|
||||||
|
const loadSet = cheapPathOn ? cheapSet : computeCheapPlan(roots).set;
|
||||||
|
renderPeopleBar(roots, docTags, loadSet, overload);
|
||||||
|
} else {
|
||||||
|
peopleBar.hidden = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings = warnings.slice().sort((a, b) => (a.line || 0) - (b.line || 0));
|
warnings = warnings.slice().sort((a, b) => (a.line || 0) - (b.line || 0));
|
||||||
@@ -268,6 +303,49 @@ function render(){
|
|||||||
updateFoldBtn(); /* Icon + Tooltip = nächster Schritt des Durchschalters (D75) */
|
updateFoldBtn(); /* Icon + Tooltip = nächster Schritt des Durchschalters (D75) */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Personen-Leiste (SPEC §9, D87) ----------
|
||||||
|
Je Person eine Pille mit Belastungs-Balken; ein Tipp schaltet die Linse
|
||||||
|
(nur ihre Knoten, alles andere gefaltet), der zweite hebt sie auf, ein
|
||||||
|
Tipp auf eine andere wechselt. Angezeigt werden ANTEILE, keine absoluten
|
||||||
|
Zahlen — die Größen sind ordinal, jede Summe ist eine Näherung (D46).
|
||||||
|
Der Rest ohne Zuständigen bekommt einen eigenen Eintrag, damit die Balken
|
||||||
|
ehrlich auf 100 % summieren; sein Klick zeigt, wofür noch niemand
|
||||||
|
eingeteilt ist. Sortiert nach Last, bei Gleichstand Dokumentreihenfolge. */
|
||||||
|
function renderPeopleBar(roots, docTags, loadSet, overload){
|
||||||
|
const {loads, total} = assigneeLoads(roots, loadSet);
|
||||||
|
const share = l => total > 0 ? Math.round(l / total * 100) : 0;
|
||||||
|
const entries = docTags.map(tg => ({tag: tg, pct: share(loads.get(tg) || 0)}))
|
||||||
|
.sort((a, b) => b.pct - a.pct || docTags.indexOf(a.tag) - docTags.indexOf(b.tag));
|
||||||
|
let assigned = 0;
|
||||||
|
loads.forEach(l => { assigned += l; });
|
||||||
|
const rest = total - assigned;
|
||||||
|
if(rest > 0) entries.push({tag: null, pct: share(rest)});
|
||||||
|
peopleBar.innerHTML = entries.map(e => {
|
||||||
|
const active = !!lens && lens.tag === e.tag;
|
||||||
|
const label = e.tag === null ? t('peopleUnassigned') : e.tag;
|
||||||
|
const tip = t('peopleShare', {share: e.pct}) + ' · ' +
|
||||||
|
t(active ? 'peopleLensOff' : 'peopleLensOn');
|
||||||
|
const warm = overload && e.tag !== null && overload.tag === e.tag;
|
||||||
|
return `<button type="button" class="pbperson" aria-pressed="${active}"` +
|
||||||
|
(e.tag === null ? ' data-nobody' : ` data-tag="${esc(e.tag)}"`) +
|
||||||
|
` title="${esc(label)}: ${esc(tip)}" aria-label="${esc(label)}: ${esc(tip)}">` +
|
||||||
|
`<span class="tag${warm ? ' overload' : ''}${e.tag === null ? ' nobody' : ''}" aria-hidden="true">${esc(label)}</span>` +
|
||||||
|
`<span class="pbbar" aria-hidden="true"><span class="pbfill" style="width:${e.pct}%"></span></span>` +
|
||||||
|
`<span class="pbpct" aria-hidden="true">${e.pct}%</span></button>`;
|
||||||
|
}).join('');
|
||||||
|
peopleBar.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
peopleBar.addEventListener('click', e => {
|
||||||
|
const b = e.target && e.target.closest ? e.target.closest('.pbperson') : null;
|
||||||
|
if(!b) return;
|
||||||
|
const tag = ('nobody' in b.dataset) ? null : b.dataset.tag;
|
||||||
|
const same = !!lens && lens.tag === tag;
|
||||||
|
clearLens();
|
||||||
|
if(!same) lens = {tag};
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
/* Fokusmarke `!!!` (SPEC §1): Der erste markierte Knoten wird ins Bild geholt —
|
/* Fokusmarke `!!!` (SPEC §1): Der erste markierte Knoten wird ins Bild geholt —
|
||||||
aber nur, wenn sich die Marke **geändert** hat. Sonst zöge jeder Neubau des
|
aber nur, wenn sich die Marke **geändert** hat. Sonst zöge jeder Neubau des
|
||||||
Baums den Blick zurück und man käme nicht weg. Verglichen wird das Label des
|
Baums den Blick zurück und man käme nicht weg. Verglichen wird das Label des
|
||||||
@@ -1290,6 +1368,10 @@ function writeAllFoldMarks(roots, want){
|
|||||||
bei ausgeschaltetem Pfad-Umschalter (die Voreinstellung fragt nach dem
|
bei ausgeschaltetem Pfad-Umschalter (die Voreinstellung fragt nach dem
|
||||||
Pfad, nicht nach seiner Anzeige). */
|
Pfad, nicht nach seiner Anzeige). */
|
||||||
function applyFoldPreset(mode){
|
function applyFoldPreset(mode){
|
||||||
|
/* Der Durchschalter arbeitet auf dem TEXT-Faltzustand — eine aktive
|
||||||
|
Personen-Linse (D87) endet damit; sonst schriebe er Marken, die man
|
||||||
|
unter der Linse gar nicht sieht. */
|
||||||
|
clearLens();
|
||||||
const roots = parse(src.value).roots;
|
const roots = parse(src.value).roots;
|
||||||
if(!roots.length) return;
|
if(!roots.length) return;
|
||||||
const cs = mode === 'path' ? computeCheapPlan(roots).set : null;
|
const cs = mode === 'path' ? computeCheapPlan(roots).set : null;
|
||||||
@@ -1310,6 +1392,16 @@ function toggleFold(el){
|
|||||||
const line = +el.dataset.line;
|
const line = +el.dataset.line;
|
||||||
const st = foldByLine.get(line);
|
const st = foldByLine.get(line);
|
||||||
if(!st || !st.canFold) return;
|
if(!st || !st.canFold) return;
|
||||||
|
/* Bei aktiver Personen-Linse (D87) bleibt die Hand-Faltung in deren
|
||||||
|
Sitzungs-Überlagerung — in den Text geschrieben würde sonst der
|
||||||
|
persönliche Filter, in geteilten Dokumenten für alle. */
|
||||||
|
if(lens){
|
||||||
|
lensOverrides.set(st.key, !st.collapsed);
|
||||||
|
render();
|
||||||
|
const again2 = out.querySelector('.node[data-line="' + line + '"]');
|
||||||
|
if(again2) again2.focus({preventScroll: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
foldOverrides.set(st.key, !st.collapsed);
|
foldOverrides.set(st.key, !st.collapsed);
|
||||||
/* Das Schreiben löst per `input`-Ereignis schon ein render() aus. */
|
/* Das Schreiben löst per `input`-Ereignis schon ein render() aus. */
|
||||||
if(writeFoldToText(line, !st.collapsed)) foldOverrides.clear();
|
if(writeFoldToText(line, !st.collapsed)) foldOverrides.clear();
|
||||||
@@ -2326,6 +2418,11 @@ const I18N = {
|
|||||||
sizeConflictTooltip:"Die Teilpakete übersteigen zusammen die angegebene Größe",
|
sizeConflictTooltip:"Die Teilpakete übersteigen zusammen die angegebene Größe",
|
||||||
cheapApproxWarn:"Zu viele gekoppelte Alternativgruppen für die exakte Suche — der günstigste Pfad ist gierig geschätzt (je Gruppe lokal gewählt).",
|
cheapApproxWarn:"Zu viele gekoppelte Alternativgruppen für die exakte Suche — der günstigste Pfad ist gierig geschätzt (je Gruppe lokal gewählt).",
|
||||||
assigneeOverloadWarn:"@{tag} trägt {share} % der offenen Arbeit auf dem günstigsten Pfad ({stations} von {total} Stationen) — mögliche Engstelle.",
|
assigneeOverloadWarn:"@{tag} trägt {share} % der offenen Arbeit auf dem günstigsten Pfad ({stations} von {total} Stationen) — mögliche Engstelle.",
|
||||||
|
peopleBarLabel:"Zuständige",
|
||||||
|
peopleUnassigned:"ohne Zuständigen",
|
||||||
|
peopleShare:"{share} % der offenen Arbeit auf dem günstigsten Pfad",
|
||||||
|
peopleLensOn:"Klick: nur diese Knoten zeigen, alles andere zuklappen",
|
||||||
|
peopleLensOff:"Klick: Filter aufheben",
|
||||||
st_idee:"Idee", st_geplant:"geplant", st_arbeit:"in Arbeit", st_durchstich:"Durchstich",
|
st_idee:"Idee", st_geplant:"geplant", st_arbeit:"in Arbeit", st_durchstich:"Durchstich",
|
||||||
st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen",
|
st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen",
|
||||||
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
||||||
@@ -2447,6 +2544,11 @@ const I18N = {
|
|||||||
sizeConflictTooltip:"The sub-packages together exceed the given size",
|
sizeConflictTooltip:"The sub-packages together exceed the given size",
|
||||||
cheapApproxWarn:"Too many coupled alternative groups for the exact search — the cheapest path is a greedy estimate (chosen locally per group).",
|
cheapApproxWarn:"Too many coupled alternative groups for the exact search — the cheapest path is a greedy estimate (chosen locally per group).",
|
||||||
assigneeOverloadWarn:"@{tag} carries {share}% of the open work on the cheapest path ({stations} of {total} stations) — a possible bottleneck.",
|
assigneeOverloadWarn:"@{tag} carries {share}% of the open work on the cheapest path ({stations} of {total} stations) — a possible bottleneck.",
|
||||||
|
peopleBarLabel:"Assignees",
|
||||||
|
peopleUnassigned:"unassigned",
|
||||||
|
peopleShare:"{share}% of the open work on the cheapest path",
|
||||||
|
peopleLensOn:"Click: show only these nodes, fold everything else",
|
||||||
|
peopleLensOff:"Click: remove the filter",
|
||||||
st_idee:"idea", st_geplant:"planned", st_arbeit:"in progress", st_durchstich:"walking skeleton",
|
st_idee:"idea", st_geplant:"planned", st_arbeit:"in progress", st_durchstich:"walking skeleton",
|
||||||
st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded",
|
st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded",
|
||||||
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
||||||
@@ -2568,6 +2670,11 @@ const I18N = {
|
|||||||
sizeConflictTooltip:"Los subpaquetes juntos superan el tamaño indicado",
|
sizeConflictTooltip:"Los subpaquetes juntos superan el tamaño indicado",
|
||||||
cheapApproxWarn:"Demasiados grupos de alternativas acoplados para la búsqueda exacta — el camino más barato es una estimación voraz (elección local por grupo).",
|
cheapApproxWarn:"Demasiados grupos de alternativas acoplados para la búsqueda exacta — el camino más barato es una estimación voraz (elección local por grupo).",
|
||||||
assigneeOverloadWarn:"@{tag} lleva el {share} % del trabajo pendiente en el camino más barato ({stations} de {total} estaciones) — posible cuello de botella.",
|
assigneeOverloadWarn:"@{tag} lleva el {share} % del trabajo pendiente en el camino más barato ({stations} de {total} estaciones) — posible cuello de botella.",
|
||||||
|
peopleBarLabel:"Responsables",
|
||||||
|
peopleUnassigned:"sin responsable",
|
||||||
|
peopleShare:"{share} % del trabajo pendiente en el camino más barato",
|
||||||
|
peopleLensOn:"Clic: mostrar solo estos nodos y plegar el resto",
|
||||||
|
peopleLensOff:"Clic: quitar el filtro",
|
||||||
st_idee:"idea", st_geplant:"planificado", st_arbeit:"en curso", st_durchstich:"prototipo funcional",
|
st_idee:"idea", st_geplant:"planificado", st_arbeit:"en curso", st_durchstich:"prototipo funcional",
|
||||||
st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado",
|
st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado",
|
||||||
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
|
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
|
||||||
@@ -2689,6 +2796,11 @@ const I18N = {
|
|||||||
sizeConflictTooltip:"Les sous-lots dépassent ensemble la taille indiquée",
|
sizeConflictTooltip:"Les sous-lots dépassent ensemble la taille indiquée",
|
||||||
cheapApproxWarn:"Trop de groupes d’alternatives couplés pour la recherche exacte — le chemin le moins cher est une estimation gloutonne (choix local par groupe).",
|
cheapApproxWarn:"Trop de groupes d’alternatives couplés pour la recherche exacte — le chemin le moins cher est une estimation gloutonne (choix local par groupe).",
|
||||||
assigneeOverloadWarn:"@{tag} porte {share} % du travail restant sur le chemin le moins cher ({stations} stations sur {total}) — goulot d’étranglement possible.",
|
assigneeOverloadWarn:"@{tag} porte {share} % du travail restant sur le chemin le moins cher ({stations} stations sur {total}) — goulot d’étranglement possible.",
|
||||||
|
peopleBarLabel:"Responsables",
|
||||||
|
peopleUnassigned:"sans responsable",
|
||||||
|
peopleShare:"{share} % du travail restant sur le chemin le moins cher",
|
||||||
|
peopleLensOn:"Clic : n’afficher que ces nœuds, replier le reste",
|
||||||
|
peopleLensOff:"Clic : retirer le filtre",
|
||||||
st_idee:"idée", st_geplant:"planifié", st_arbeit:"en cours", st_durchstich:"squelette fonctionnel",
|
st_idee:"idée", st_geplant:"planifié", st_arbeit:"en cours", st_durchstich:"squelette fonctionnel",
|
||||||
st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné",
|
st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné",
|
||||||
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
||||||
@@ -2810,6 +2922,11 @@ const I18N = {
|
|||||||
sizeConflictTooltip:"Podzadania razem przekraczają podany rozmiar",
|
sizeConflictTooltip:"Podzadania razem przekraczają podany rozmiar",
|
||||||
cheapApproxWarn:"Zbyt wiele sprzężonych grup alternatyw dla dokładnego wyszukiwania — najtańsza ścieżka jest oszacowana zachłannie (wybór lokalny w każdej grupie).",
|
cheapApproxWarn:"Zbyt wiele sprzężonych grup alternatyw dla dokładnego wyszukiwania — najtańsza ścieżka jest oszacowana zachłannie (wybór lokalny w każdej grupie).",
|
||||||
assigneeOverloadWarn:"@{tag} niesie {share} % otwartej pracy na najtańszej ścieżce ({stations} z {total} stacji) — możliwe wąskie gardło.",
|
assigneeOverloadWarn:"@{tag} niesie {share} % otwartej pracy na najtańszej ścieżce ({stations} z {total} stacji) — możliwe wąskie gardło.",
|
||||||
|
peopleBarLabel:"Odpowiedzialni",
|
||||||
|
peopleUnassigned:"bez odpowiedzialnego",
|
||||||
|
peopleShare:"{share} % otwartej pracy na najtańszej ścieżce",
|
||||||
|
peopleLensOn:"Klik: pokaż tylko te węzły, resztę zwiń",
|
||||||
|
peopleLensOff:"Klik: usuń filtr",
|
||||||
st_idee:"pomysł", st_geplant:"zaplanowane", st_arbeit:"w toku", st_durchstich:"działający szkielet",
|
st_idee:"pomysł", st_geplant:"zaplanowane", st_arbeit:"w toku", st_durchstich:"działający szkielet",
|
||||||
st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone",
|
st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone",
|
||||||
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
||||||
@@ -2931,6 +3048,11 @@ const I18N = {
|
|||||||
sizeConflictTooltip:"Подзадачи вместе превышают указанный размер",
|
sizeConflictTooltip:"Подзадачи вместе превышают указанный размер",
|
||||||
cheapApproxWarn:"Слишком много связанных групп альтернатив для точного поиска — самый дешёвый путь оценён жадно (локальный выбор в каждой группе).",
|
cheapApproxWarn:"Слишком много связанных групп альтернатив для точного поиска — самый дешёвый путь оценён жадно (локальный выбор в каждой группе).",
|
||||||
assigneeOverloadWarn:"@{tag} несёт {share} % открытой работы на самом дешёвом пути ({stations} из {total} станций) — возможное узкое место.",
|
assigneeOverloadWarn:"@{tag} несёт {share} % открытой работы на самом дешёвом пути ({stations} из {total} станций) — возможное узкое место.",
|
||||||
|
peopleBarLabel:"Ответственные",
|
||||||
|
peopleUnassigned:"без ответственного",
|
||||||
|
peopleShare:"{share} % открытой работы на самом дешёвом пути",
|
||||||
|
peopleLensOn:"Клик: показать только эти узлы, остальное свернуть",
|
||||||
|
peopleLensOff:"Клик: снять фильтр",
|
||||||
st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
|
st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
|
||||||
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
|
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
|
||||||
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
||||||
@@ -3052,6 +3174,11 @@ const I18N = {
|
|||||||
sizeConflictTooltip:"उप-पैकेज मिलकर दिए गए आकार से बड़े हैं",
|
sizeConflictTooltip:"उप-पैकेज मिलकर दिए गए आकार से बड़े हैं",
|
||||||
cheapApproxWarn:"सटीक खोज के लिए बहुत सारे युग्मित विकल्प-समूह — सबसे सस्ता पथ लालची अनुमान है (प्रति समूह स्थानीय चयन)।",
|
cheapApproxWarn:"सटीक खोज के लिए बहुत सारे युग्मित विकल्प-समूह — सबसे सस्ता पथ लालची अनुमान है (प्रति समूह स्थानीय चयन)।",
|
||||||
assigneeOverloadWarn:"@{tag} सबसे सस्ते पथ पर खुले काम का {share}% उठाए हुए है ({total} में से {stations} स्टेशन) — संभावित अड़चन।",
|
assigneeOverloadWarn:"@{tag} सबसे सस्ते पथ पर खुले काम का {share}% उठाए हुए है ({total} में से {stations} स्टेशन) — संभावित अड़चन।",
|
||||||
|
peopleBarLabel:"ज़िम्मेदार",
|
||||||
|
peopleUnassigned:"बिना ज़िम्मेदार",
|
||||||
|
peopleShare:"सबसे सस्ते पथ पर खुले काम का {share}%",
|
||||||
|
peopleLensOn:"क्लिक: केवल ये नोड दिखाएँ, बाकी सब समेटें",
|
||||||
|
peopleLensOff:"क्लिक: फ़िल्टर हटाएँ",
|
||||||
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
|
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
|
||||||
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
|
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
|
||||||
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
||||||
@@ -3173,6 +3300,11 @@ const I18N = {
|
|||||||
sizeConflictTooltip:"子项合计超出所标注的尺寸",
|
sizeConflictTooltip:"子项合计超出所标注的尺寸",
|
||||||
cheapApproxWarn:"耦合的备选组过多,无法精确搜索——最便宜路径为贪心估计(每组就地选择)。",
|
cheapApproxWarn:"耦合的备选组过多,无法精确搜索——最便宜路径为贪心估计(每组就地选择)。",
|
||||||
assigneeOverloadWarn:"@{tag} 承担最便宜路径上 {share}% 的未完成工作({total} 个站点中的 {stations} 个)——可能的瓶颈。",
|
assigneeOverloadWarn:"@{tag} 承担最便宜路径上 {share}% 的未完成工作({total} 个站点中的 {stations} 个)——可能的瓶颈。",
|
||||||
|
peopleBarLabel:"负责人",
|
||||||
|
peopleUnassigned:"未分配",
|
||||||
|
peopleShare:"最便宜路径上未完成工作的 {share}%",
|
||||||
|
peopleLensOn:"点击:只显示这些节点,折叠其余部分",
|
||||||
|
peopleLensOff:"点击:取消筛选",
|
||||||
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
|
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
|
||||||
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
|
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
|
||||||
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
||||||
@@ -3294,6 +3426,11 @@ const I18N = {
|
|||||||
sizeConflictTooltip:"サブパッケージの合計が指定サイズを超えています",
|
sizeConflictTooltip:"サブパッケージの合計が指定サイズを超えています",
|
||||||
cheapApproxWarn:"結合された選択肢グループが多すぎるため厳密探索は不可 — 最安パスは貪欲法による推定です(グループごとに局所選択)。",
|
cheapApproxWarn:"結合された選択肢グループが多すぎるため厳密探索は不可 — 最安パスは貪欲法による推定です(グループごとに局所選択)。",
|
||||||
assigneeOverloadWarn:"@{tag} が最安パスの未完了作業の {share}% を担っています(全 {total} 駅中 {stations} 駅)— ボトルネックの可能性。",
|
assigneeOverloadWarn:"@{tag} が最安パスの未完了作業の {share}% を担っています(全 {total} 駅中 {stations} 駅)— ボトルネックの可能性。",
|
||||||
|
peopleBarLabel:"担当者",
|
||||||
|
peopleUnassigned:"担当者なし",
|
||||||
|
peopleShare:"最安パスの未完了作業の {share}%",
|
||||||
|
peopleLensOn:"クリック:このノードだけを表示し、他を折りたたむ",
|
||||||
|
peopleLensOff:"クリック:フィルターを解除",
|
||||||
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
|
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
|
||||||
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
|
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
|
||||||
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
||||||
@@ -4237,6 +4374,7 @@ document.addEventListener('pointerdown', e => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : '';
|
function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : '';
|
||||||
|
clearLens(); /* die Personen-Linse (D87) gilt je Dokument-Sitzung */
|
||||||
/* Vergleichsstand für den nächsten Snapshot (D54): Ohne ihn legte der
|
/* Vergleichsstand für den nächsten Snapshot (D54): Ohne ihn legte der
|
||||||
erste Takt nach dem Öffnen auch ein unverändertes Dokument weg. */
|
erste Takt nach dem Öffnen auch ein unverändertes Dokument weg. */
|
||||||
snapBase = src.value; closeSnapMenu();
|
snapBase = src.value; closeSnapMenu();
|
||||||
|
|||||||
@@ -528,6 +528,57 @@ export function presetFoldSet(roots, mode, cheapSet){
|
|||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Personen-Linse (SPEC §9, D87) ----------
|
||||||
|
Vollständiger Faltzustand für „zeig mir die Arbeit von @tag": offen bleiben
|
||||||
|
nur die VORFAHREN der Knoten, die den Tag tragen; die getaggten Knoten
|
||||||
|
selbst klappen zu — ihr „▸ n" vertritt das Paket, und mit der Vererbung
|
||||||
|
(D71: wer das Paket hat, hat die Teilpakete) ist alles darunter ohnehin
|
||||||
|
ihres. Ein getaggter Knoten OHNE Kinder bleibt als Blatt stehen (es gibt
|
||||||
|
nichts zu falten); nicht faltbare Geschwister auf offenen Pfaden bleiben
|
||||||
|
sichtbar — dieselbe Eigenschaft wie beim 'path'-Preset (D75), die
|
||||||
|
Pillen-Hervorhebung sagt, welche gemeint sind.
|
||||||
|
`tag === null` ist die Unzugewiesen-Linse: Jeder Knoten mit EIGENEN Tags
|
||||||
|
klappt zu (die Grenze ins zugewiesene Gebiet) — sichtbar bleibt die
|
||||||
|
Struktur, deren Marge niemandem gehört. */
|
||||||
|
export function personFoldSet(roots, tag){
|
||||||
|
const set = new Set();
|
||||||
|
if(tag === null){
|
||||||
|
const walk = ns => {
|
||||||
|
for(const n of ns){
|
||||||
|
if(n.children.length && n.tags && n.tags.length) set.add(n);
|
||||||
|
walk(n.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(roots);
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
const hasTag = n => (n.tags || []).includes(tag) || n.children.some(hasTag);
|
||||||
|
const walk = ns => {
|
||||||
|
for(const n of ns){
|
||||||
|
if(n.children.length){
|
||||||
|
if((n.tags || []).includes(tag) || !hasTag(n)) set.add(n);
|
||||||
|
walk(n.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(roots);
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alle vergebenen Personen-Tags in Dokumentreihenfolge — die Einträge der
|
||||||
|
Personen-Leiste (D87). */
|
||||||
|
export function allTags(roots){
|
||||||
|
const tags = [];
|
||||||
|
const walk = ns => {
|
||||||
|
for(const n of ns){
|
||||||
|
for(const tg of n.tags || []) if(!tags.includes(tg)) tags.push(tg);
|
||||||
|
walk(n.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(roots);
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
/* key -> Status-Schlüssel ('' für neutrale Knoten) über den ganzen Baum. */
|
/* key -> Status-Schlüssel ('' für neutrale Knoten) über den ganzen Baum. */
|
||||||
export function statusByKey(roots){
|
export function statusByKey(roots){
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
|
|||||||
@@ -208,8 +208,13 @@ function nodeHtml(n, extra, opts, fold){
|
|||||||
Nur direkte Tags haben eine Pille; geerbte Zuständigkeit (die in die
|
Nur direkte Tags haben eine Pille; geerbte Zuständigkeit (die in die
|
||||||
Rechnung eingeht) hat nichts, das sich färben ließe. */
|
Rechnung eingeht) hat nichts, das sich färben ließe. */
|
||||||
const hot = tag => opts.overloadTag === tag && opts.cheapSet.has(n) && !isDone(n);
|
const hot = tag => opts.overloadTag === tag && opts.cheapSet.has(n) && !isDone(n);
|
||||||
|
/* Personen-Linse (SPEC §9/D87): die Pille der gewählten Person hebt sich in
|
||||||
|
Petrol — sie sagt, WELCHE der sichtbaren Knoten gemeint sind (nicht
|
||||||
|
faltbare Geschwister bleiben ja stehen). Die Warnfarbe des Engpasses
|
||||||
|
gewinnt: eine Warnung wiegt schwerer als eine Auswahl. */
|
||||||
|
const pillCls = tag => hot(tag) ? ' overload' : (opts.lensTag === tag ? ' lens' : '');
|
||||||
const tagsHtml = n.tags && n.tags.length
|
const tagsHtml = n.tags && n.tags.length
|
||||||
? `<span class="tags" aria-hidden="true">${n.tags.map(tag => `<span class="tag${hot(tag) ? ' overload' : ''}">${esc(tag)}</span>`).join('')}</span>`
|
? `<span class="tags" aria-hidden="true">${n.tags.map(tag => `<span class="tag${pillCls(tag)}">${esc(tag)}</span>`).join('')}</span>`
|
||||||
: '';
|
: '';
|
||||||
/* Das implizite Größen-Badge macht eine KOSTENANNAHME sichtbar (D18) —
|
/* Das implizite Größen-Badge macht eine KOSTENANNAHME sichtbar (D18) —
|
||||||
seit D66 die aus den Teilpaketen geschätzte Größe statt pauschal M. An
|
seit D66 die aus den Teilpaketen geschätzte Größe statt pauschal M. An
|
||||||
|
|||||||
+32
-1
@@ -641,6 +641,33 @@
|
|||||||
.panel.right{background:rgba(255,255,255,.6);position:relative}
|
.panel.right{background:rgba(255,255,255,.6);position:relative}
|
||||||
.warnings{padding:0 16px 14px;font-size:.8rem;color:var(--warn)}
|
.warnings{padding:0 16px 14px;font-size:.8rem;color:var(--warn)}
|
||||||
.warnings div{margin-top:4px}
|
.warnings div{margin-top:4px}
|
||||||
|
/* Personen-Leiste (SPEC §9/D87): je Person eine Pille mit Belastungs-Balken,
|
||||||
|
Tipp schaltet die Personen-Linse. Erscheint nur bei Dokumenten mit
|
||||||
|
@-Tags; auf dem Telefon scrollt sie waagerecht statt umzubrechen (kein
|
||||||
|
Aufklapp-Inhalt darin, die D50-Falle greift also nicht). */
|
||||||
|
.peoplebar{
|
||||||
|
display:flex;gap:8px;align-items:center;flex-wrap:wrap;
|
||||||
|
padding:0 16px 10px;
|
||||||
|
}
|
||||||
|
.pbperson{
|
||||||
|
display:flex;gap:7px;align-items:center;
|
||||||
|
border:1px solid rgba(36,52,71,.18);border-radius:999px;
|
||||||
|
background:var(--card);cursor:pointer;
|
||||||
|
padding:3px 10px 3px 4px;font:inherit;
|
||||||
|
}
|
||||||
|
.pbperson:hover{border-color:var(--or)}
|
||||||
|
.pbperson[aria-pressed="true"]{border-color:var(--or);box-shadow:0 0 0 1.5px var(--or);background:rgba(15,118,110,.06)}
|
||||||
|
.pbperson .tag{box-shadow:none}
|
||||||
|
/* „ohne Zuständigen": gestrichelte Pille — es ist ein Loch, keine Person. */
|
||||||
|
.pbperson .tag.nobody{border-style:dashed;color:var(--muted);background:var(--card)}
|
||||||
|
.pbbar{
|
||||||
|
width:64px;height:6px;border-radius:999px;overflow:hidden;
|
||||||
|
background:rgba(36,52,71,.1);
|
||||||
|
}
|
||||||
|
.pbfill{display:block;height:100%;background:var(--or);border-radius:999px}
|
||||||
|
.pbpct{font-size:.68rem;color:var(--muted);min-width:3ch;text-align:right}
|
||||||
|
body.mobile .peoplebar{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;padding:0 10px 8px}
|
||||||
|
.panel.collapsed .peoplebar{display:none}
|
||||||
.empty{color:var(--muted);font-size:.85rem;padding:8px 4px}
|
.empty{color:var(--muted);font-size:.85rem;padding:8px 4px}
|
||||||
|
|
||||||
/* Zoom-Steuerung: schwebt unten rechts über dem Diagramm. */
|
/* Zoom-Steuerung: schwebt unten rechts über dem Diagramm. */
|
||||||
@@ -890,6 +917,10 @@
|
|||||||
Hälfte der offenen Pfad-Arbeit trägt — Warnfarbe wie das
|
Hälfte der offenen Pfad-Arbeit trägt — Warnfarbe wie das
|
||||||
Größen-Konflikt-Badge (D62); der Export liest die gemessenen Farben. */
|
Größen-Konflikt-Badge (D62); der Export liest die gemessenen Farben. */
|
||||||
.tag.overload{background:var(--warn);color:#fff;border-color:var(--warn)}
|
.tag.overload{background:var(--warn);color:#fff;border-color:var(--warn)}
|
||||||
|
/* Personen-Linse (D87): die Pille der gewählten Person in Petrol — welche
|
||||||
|
der sichtbaren Knoten ihr gehören. Die Engpass-Warnfarbe gewinnt (der
|
||||||
|
Renderer vergibt nur eine der beiden Klassen). */
|
||||||
|
.tag.lens{background:var(--or);color:#fff;border-color:var(--or)}
|
||||||
.ghost-node{
|
.ghost-node{
|
||||||
position:relative;margin-top:14px;
|
position:relative;margin-top:14px;
|
||||||
border:1.5px dashed var(--warn);
|
border:1.5px dashed var(--warn);
|
||||||
@@ -1551,7 +1582,7 @@
|
|||||||
@media print{
|
@media print{
|
||||||
html,body{height:auto!important;overflow:visible!important;background:#fff!important}
|
html,body{height:auto!important;overflow:visible!important;background:#fff!important}
|
||||||
header,.panel.editor,.gutter,.panel.right .panel-head,
|
header,.panel.editor,.gutter,.panel.right .panel-head,
|
||||||
.warnings,.zoomctl,.site-footer,.nodetip,.aclist{display:none!important}
|
.warnings,.zoomctl,.site-footer,.nodetip,.aclist,.peoplebar{display:none!important}
|
||||||
.app,.app.side{display:block!important;max-width:none!important;width:auto!important;
|
.app,.app.side{display:block!important;max-width:none!important;width:auto!important;
|
||||||
height:auto!important;margin:0!important;gap:0!important}
|
height:auto!important;margin:0!important;gap:0!important}
|
||||||
.panel.right{grid-area:auto!important;background:#fff!important;border:0!important;
|
.panel.right{grid-area:auto!important;background:#fff!important;border:0!important;
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parse } from '../src/parser.js';
|
||||||
|
import { personFoldSet, allTags } from '../src/model.js';
|
||||||
|
|
||||||
|
/* Personen-Linse (SPEC §9, D87): personFoldSet liefert einen VOLLSTÄNDIGEN
|
||||||
|
Faltzustand — offen bleiben nur die Vorfahren der getaggten Knoten, die
|
||||||
|
getaggten selbst klappen zu; tag === null ist die Unzugewiesen-Linse. */
|
||||||
|
|
||||||
|
const PLAN = `Projekt
|
||||||
|
- Backend @anna
|
||||||
|
- Auth (S)
|
||||||
|
- API (M) @ben
|
||||||
|
- Frontend @ben
|
||||||
|
- Views (S)
|
||||||
|
- Detail (XS)
|
||||||
|
- Infra
|
||||||
|
- CI (S) @anna
|
||||||
|
- Deploy (S)
|
||||||
|
- Doku (S)
|
||||||
|
`;
|
||||||
|
|
||||||
|
function byLabel(roots){
|
||||||
|
const map = new Map();
|
||||||
|
const walk = ns => { for(const n of ns){ map.set(n.label, n); walk(n.children); } };
|
||||||
|
walk(roots);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
function labels(set){ return [...set].map(n => n.label).sort(); }
|
||||||
|
|
||||||
|
describe('personFoldSet — Personen-Linse (D87)', () => {
|
||||||
|
const roots = parse(PLAN).roots;
|
||||||
|
const N = byLabel(roots);
|
||||||
|
|
||||||
|
it('getaggte Knoten klappen zu, ihre Vorfahren bleiben offen', () => {
|
||||||
|
const set = personFoldSet(roots, 'anna');
|
||||||
|
expect(set.has(N.get('Backend'))).toBe(true); /* @anna, zu — ▸ vertritt das Paket */
|
||||||
|
expect(set.has(N.get('Projekt'))).toBe(false); /* Vorfahr von annas Knoten */
|
||||||
|
expect(set.has(N.get('Infra'))).toBe(false); /* Vorfahr von annas CI */
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Teilbäume ohne den Tag klappen an ihrer Wurzel zu', () => {
|
||||||
|
const set = personFoldSet(roots, 'anna');
|
||||||
|
expect(set.has(N.get('Frontend'))).toBe(true); /* @ben — nichts von anna darin */
|
||||||
|
expect(labels(set)).toContain('Views'); /* auch tiefer: vollständiger Zustand */
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein getaggtes Blatt ist nicht in der Menge — es gibt nichts zu falten', () => {
|
||||||
|
const set = personFoldSet(roots, 'anna');
|
||||||
|
expect(set.has(N.get('CI'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Vorfahr mit fremdem Tag bleibt offen, wenn der gesuchte Tag darunter liegt', () => {
|
||||||
|
const set = personFoldSet(roots, 'ben');
|
||||||
|
expect(set.has(N.get('Backend'))).toBe(false); /* enthält bens API */
|
||||||
|
expect(set.has(N.get('Frontend'))).toBe(true); /* @ben selbst — zu */
|
||||||
|
expect(set.has(N.get('Infra'))).toBe(true); /* nichts von ben */
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Unzugewiesen-Linse: jeder Knoten mit eigenen Tags klappt zu', () => {
|
||||||
|
const set = personFoldSet(roots, null);
|
||||||
|
expect(labels(set)).toEqual(['Backend', 'Frontend']); /* CI ist Blatt, Infra untagged */
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('allTags — Einträge der Personen-Leiste (D87)', () => {
|
||||||
|
it('liefert die Tags in Dokumentreihenfolge, ohne Doppelte', () => {
|
||||||
|
expect(allTags(parse(PLAN).roots)).toEqual(['anna', 'ben']);
|
||||||
|
});
|
||||||
|
it('leer ohne Tags', () => {
|
||||||
|
expect(allTags(parse('- A\n - B').roots)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user