feat: Faltmarken (>/<) — Teilbäume ein-/ausklappbar (SPEC §1/§9, D38)
Parser liest die Marke zwischen Zeichen und Statusbox (Leerraum-Regel, `- >Achtung` bleibt Label); den Anfangszustand rechnet initialCollapsed() in model.js: `>` klappt ein, `<` (und die Fokusmarke !!!) holt seinen Teilbaum hervor, indem die Faltung die Pfad-Ebenen hinunterwandert — Geschwister stehen als einzelne eingeklappte Knoten da, jede Kante bleibt eine echte. Im Diagramm: Falt-Zeichen ▾/„▸ n" vor dem Label (n = alle verborgenen Knoten), Klick oder ←/→ am fokussierten Knoten klappt um; Eingriffe je Label-Pfad, nur für die Sitzung, Dokumentwechsel setzt zurück. Eingeklappte Kinder werden nicht gerendert (Export, Messungen, Pfadlinie automatisch konsistent), ihre Warnungen aber weiter gemeldet. Export/Druck: „▸ n" bleibt, das ▾ offener Knoten fällt weg. Legende + a11y in allen 9 Sprachen; SPEC-§11-Abschnitt in §1/§9 überführt; 15 neue Tests (tests/fold.test.js), Snapshots aktualisiert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c778e3e267
commit
cbe798d746
@@ -1771,3 +1771,66 @@ auch die Selbst-Abhängigkeit ist stumm.
|
|||||||
dieselbe Zurückhaltung wie bei den IDs (D36): keine eigene Diagramm-
|
dieselbe Zurückhaltung wie bei den IDs (D36): keine eigene Diagramm-
|
||||||
Darstellung, bis die Querverbindungen (§11) gebaut sind; der Pfeil im Tooltip
|
Darstellung, bis die Querverbindungen (§11) gebaut sind; der Pfeil im Tooltip
|
||||||
sagt die Richtung („hängt ab von“), ohne ein neues Zeichen einzuführen.
|
sagt die Richtung („hängt ab von“), ohne ein neues Zeichen einzuführen.
|
||||||
|
|
||||||
|
## D38 — Faltmarken gebaut: `<` wandert die Faltung hinunter, Eingriffe sind flüchtig
|
||||||
|
Die in D34 entschiedene Schreibweise (`- > [x] …`, Export folgt der Faltung)
|
||||||
|
ist umgesetzt (SPEC §1/§9). Die Bau-Entscheidungen:
|
||||||
|
|
||||||
|
**`<` holt seinen Teilbaum hervor, indem die Faltung die Pfad-Ebenen
|
||||||
|
hinunterwandert.** Die naive Lesart — `<` öffnet einfach alle Vorfahren —
|
||||||
|
machte das `>` am Vorfahren wirkungslos: Öffnen zeigt **alle** Kinder samt
|
||||||
|
Teilbäumen, nicht den einen gemeinten. Die andere naheliegende Lesart — nur
|
||||||
|
den Pfad einblenden, alles andere desselben Vorfahren verbergen — bräuchte
|
||||||
|
Knoten, die ohne ihre sichtbaren Eltern gezeichnet werden, oder Kanten, die
|
||||||
|
Ebenen überspringen: Beides log über die Struktur (dieselbe Sorte Fehler, die
|
||||||
|
D29 beim Treppen-Export korrigiert hat). Das Hinunterwandern vermeidet beides:
|
||||||
|
Der eingeklappte Vorfahr öffnet sich, seine **übrigen** Kinder werden
|
||||||
|
stattdessen eingeklappt — sichtbar ist der Pfad samt Teilbaum, die Geschwister
|
||||||
|
stehen als je ein Knoten mit „▸ n“ da, und jede gezeichnete Kante ist eine
|
||||||
|
echte. Ein `>` innerhalb des hervorgeholten Teilbaums bleibt respektiert.
|
||||||
|
Ausdrückbar ist das alles in einem schlichten Je-Knoten-Zustand
|
||||||
|
(`initialCollapsed()` in model.js, headless getestet).
|
||||||
|
|
||||||
|
**Auch die Fokusmarke `!!!` holt sich hervor** — über denselben Mechanismus
|
||||||
|
(sie geht als zweite Rettungsmarke in `initialCollapsed()` ein). Ein
|
||||||
|
Zeigefinger auf etwas Unsichtbares zeigte ins Leere; und weil Nutzer-Eingriffe
|
||||||
|
den Anfangszustand **überlagern**, kann man den Bereich danach trotzdem wieder
|
||||||
|
zuklappen — die Marke reißt ihn nicht bei jedem Neubau wieder auf.
|
||||||
|
|
||||||
|
**Interaktive Eingriffe: je Knoten, Label-Pfad-Identität, nur für die
|
||||||
|
Sitzung.** Der Baum wird bei jedem Tastendruck neu geparst — ein Eingriff muss
|
||||||
|
Neu-Renderings überleben, also hängt er nicht am Knotenobjekt, sondern am
|
||||||
|
**Label-Pfad** (dieselbe Identität wie „Was ist neu?“, D28: Umsortieren
|
||||||
|
überlebt, Umbenennen gilt als neuer Knoten). **Nicht persistiert**: Die
|
||||||
|
dauerhafte Aussage über den Anfangszustand steht im Text (D34 — „so wird
|
||||||
|
dieses Dokument eröffnet“); der localStorage-Ansichtszustand ist zudem global
|
||||||
|
über alle Dokumente (D22), ein Je-Dokument-Faltzustand wäre dort ein
|
||||||
|
Fremdkörper. Dokumentwechsel setzt zurück.
|
||||||
|
|
||||||
|
**Bedienung: Falt-Zeichen ▾/„▸ n“ vor dem Label, Pfeiltasten ←/→.** Der
|
||||||
|
einfache Klick ist der Link (§6), Alt+Klick der Sprung (D25), der lange Druck
|
||||||
|
ebenso — für das Falten blieb nur ein eigenes Klickziel oder eine weitere
|
||||||
|
Modifier-Geste; das sichtbare Zeichen ist zugleich die Auffindbarkeit (die
|
||||||
|
Lehre aus D25: eine Geste, die niemand sieht, ist keine Funktion). ←/→ am
|
||||||
|
fokussierten Knoten ist das WAI-ARIA-Baum-Idiom; Enter bleibt dem Link,
|
||||||
|
Alt+Enter dem Sprung. Der Zähler nennt **alle** verborgenen Knoten (nicht nur
|
||||||
|
die direkten Kinder) — er sagt, wie viel Plan dort zusammengefaltet liegt.
|
||||||
|
Nach dem Umklappen wird der Fokus auf denselben Knoten zurückgesetzt (der
|
||||||
|
Neubau hätte ihn sonst verschluckt und die Tastaturbedienung abgerissen).
|
||||||
|
|
||||||
|
**Faltung ist reine Ansicht — mit zwei bewussten Konsequenzen.** Warnungen
|
||||||
|
aus eingeklappten Teilbäumen werden weiter gemeldet: `renderChildren()`
|
||||||
|
überspringt nur das HTML, ein eigener Lauf (`walkFolded`) sammelt Warnungen
|
||||||
|
und zählt zugleich die verborgenen Knoten — sonst verschwände eine
|
||||||
|
`mixedGate`-Meldung je nach Faltzustand, obwohl sie den **Text** betrifft.
|
||||||
|
Und der günstigste Pfad rechnet unverändert über den ganzen Baum; seine
|
||||||
|
Spline-Linie läuft ohnehin nur durch DOM-Knoten, führt also durch die
|
||||||
|
sichtbaren Endknoten. Ein eingeklappter Zweig kann Pfadknoten verbergen —
|
||||||
|
hinnehmbar, die Inversion an den sichtbaren Knoten bleibt richtig.
|
||||||
|
|
||||||
|
**Export und Druck:** Verborgene Kinder stehen nicht im DOM — Export,
|
||||||
|
Stiel-Messung, Treppe und Pfadlinie sind damit ohne Zusatzcode konsistent
|
||||||
|
(derselbe Grund, aus dem der Renderer sie gar nicht erst erzeugt, statt sie
|
||||||
|
per CSS zu verstecken). Die Kennzeichnung „▸ n“ ist Teil des Knotentexts und
|
||||||
|
wandert von selbst in den SVG-Export; das ▾ offener Knoten wird dort und im
|
||||||
|
Druck entfernt — es ist Bedienelement, keine Aussage über den Plan.
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ Pfad, der die Wahrheit sagt.
|
|||||||
Anfangszustand — im Diagramm wird danach unabhängig gefaltet. Schreibweise
|
Anfangszustand — im Diagramm wird danach unabhängig gefaltet. Schreibweise
|
||||||
und Export-Verhalten sind entschieden (D34-Nachtrag): `- > [x] …`,
|
und Export-Verhalten sind entschieden (D34-Nachtrag): `- > [x] …`,
|
||||||
Export/Druck folgen der sichtbaren Faltung mit Kennzeichnung.
|
Export/Druck folgen der sichtbaren Faltung mit Kennzeichnung.
|
||||||
|
**Umgesetzt** (SPEC §1/§9, D38).
|
||||||
|
|
||||||
Der gestaffelte „günstigste Pfad" aus *Kleinere Ideen* (Status-Bewusstsein,
|
Der gestaffelte „günstigste Pfad" aus *Kleinere Ideen* (Status-Bewusstsein,
|
||||||
Ausbaustufen) baut hierauf auf: Er braucht denselben Umbau der Kostenrechnung
|
Ausbaustufen) baut hierauf auf: Er braucht denselben Umbau der Kostenrechnung
|
||||||
|
|||||||
+48
-24
@@ -7,15 +7,16 @@ Syntaxänderungen werden zuerst hier dokumentiert, dann implementiert.
|
|||||||
## 1. Zeilenformat
|
## 1. Zeilenformat
|
||||||
|
|
||||||
```
|
```
|
||||||
[Einrückung][Zeichen] [Statusbox] Label (Größe) URL @tag … !!! %% Kommentar
|
[Einrückung][Zeichen] [Faltmarke] [Statusbox] Label (Größe) URL @tag … !!! %% Kommentar
|
||||||
```
|
```
|
||||||
|
|
||||||
Alle Bestandteile außer dem Label sind optional. Die Extraktion erfolgt in
|
Alle Bestandteile außer dem Label sind optional. Die Extraktion erfolgt in
|
||||||
dieser Reihenfolge (wichtig für Kollisionsfreiheit):
|
dieser Reihenfolge (wichtig für Kollisionsfreiheit):
|
||||||
|
|
||||||
1. Kommentar entfernen: alles ab `%%` bis Zeilenende.
|
1. Kommentar entfernen: alles ab `%%` bis Zeilenende.
|
||||||
2. Einrückung, Zeichen (`-` / `+` / `|` / `=`) und Statusbox `[…]` per
|
2. Einrückung, Zeichen (`-` / `+` / `|` / `=`), Faltmarke (`>` / `<`) und
|
||||||
Zeilen-Regex; `=` nur mit folgendem Leerraum (§3).
|
Statusbox `[…]` per Zeilen-Regex; `=` nur mit folgendem Leerraum (§3),
|
||||||
|
die Faltmarke ebenso (siehe unten).
|
||||||
3. URL: erstes Token, das auf `https?://\S+` passt (dadurch stören `@` in URLs nicht).
|
3. URL: erstes Token, das auf `https?://\S+` passt (dadurch stören `@` in URLs nicht).
|
||||||
4. Größe: erstes `(XS|S|M|L|XL|XXL)`, Groß-/Kleinschreibung egal.
|
4. Größe: erstes `(XS|S|M|L|XL|XXL)`, Groß-/Kleinschreibung egal.
|
||||||
5. Tags: alle `@name`-Vorkommen.
|
5. Tags: alle `@name`-Vorkommen.
|
||||||
@@ -24,6 +25,19 @@ dieser Reihenfolge (wichtig für Kollisionsfreiheit):
|
|||||||
8. Fokusmarke: `!!!` als **alleinstehendes** Token (siehe unten).
|
8. Fokusmarke: `!!!` als **alleinstehendes** Token (siehe unten).
|
||||||
9. Rest, whitespace-normalisiert = Label. Leeres Label ⇒ Zeile ignorieren.
|
9. Rest, whitespace-normalisiert = Label. Leeres Label ⇒ Zeile ignorieren.
|
||||||
|
|
||||||
|
**Faltmarke `>` / `<`** — bestimmt, wie das Dokument **eröffnet** wird:
|
||||||
|
|
||||||
|
- Steht **zwischen** Zerlegungszeichen und Statusbox (`- > [x] Backend`); bei
|
||||||
|
Wurzelknoten (ohne Zeichen) am Zeilenanfang. Erkannt nur mit **folgendem
|
||||||
|
Leerraum** — `- >Achtung` bleibt damit ein Label. Begründung der Stellung:
|
||||||
|
D34-Nachtrag.
|
||||||
|
- `>` heißt: der Teilbaum dieses Knotens ist beim Öffnen **eingeklappt**.
|
||||||
|
- `<` innerhalb eines eingeklappten Bereichs holt den **eigenen Teilbaum**
|
||||||
|
gezielt wieder hervor (Mechanik: §9).
|
||||||
|
- Die Marken bestimmen nur den **Anfangszustand** — im Diagramm wird danach
|
||||||
|
unabhängig vom Text gefaltet (§9). Sie sagen nichts über Fortschritt (§4)
|
||||||
|
oder Notwendigkeit (§3) und ändern weder Kosten noch Warnungen.
|
||||||
|
|
||||||
**Knoten-ID `#name`** — benennt einen Knoten im **ganzen Dokument** eindeutig;
|
**Knoten-ID `#name`** — benennt einen Knoten im **ganzen Dokument** eindeutig;
|
||||||
sie ist die Adresse für Abhängigkeiten und Beschreibungsblöcke (§11).
|
sie ist die Adresse für Abhängigkeiten und Beschreibungsblöcke (§11).
|
||||||
|
|
||||||
@@ -83,7 +97,7 @@ hat etwas, das ein Cursor nicht hat: **alle** sehen dieselbe Stelle.
|
|||||||
Referenz-Regex der Implementierung:
|
Referenz-Regex der Implementierung:
|
||||||
|
|
||||||
```
|
```
|
||||||
^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:\[([ ?~xX^/-])\]\s*)?(.*)$
|
^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:([><])(?=[ \t])\s*)?(?:\[([ ?~xX^/-])\]\s*)?(.*)$
|
||||||
```
|
```
|
||||||
|
|
||||||
Für die Knoten-ID (Schritt 6, nur der erste Treffer):
|
Für die Knoten-ID (Schritt 6, nur der erste Treffer):
|
||||||
@@ -508,6 +522,36 @@ gehört und lesbar bleibt. Kein Blinken.
|
|||||||
- Wie die Cursor-Zeile erscheint sie **nicht** im Druck und **nicht** im
|
- Wie die Cursor-Zeile erscheint sie **nicht** im Druck und **nicht** im
|
||||||
Grafikexport: Sie sagt „schau jetzt hierhin", nicht „so ist der Plan".
|
Grafikexport: Sie sagt „schau jetzt hierhin", nicht „so ist der Plan".
|
||||||
|
|
||||||
|
### Ein- und ausklappbare Teilbäume (`>` / `<`, §1)
|
||||||
|
Jeder Knoten mit sichtbaren Kindern trägt ein kleines **Falt-Zeichen** vor dem
|
||||||
|
Label: **▾** offen, **„▸ n“** eingeklappt (n = Zahl der verborgenen Knoten).
|
||||||
|
Klick auf das Zeichen klappt um — der einfache Klick auf den Knoten selbst
|
||||||
|
bleibt der Link (§6); Tastatur: **←** klappt zu, **→** klappt auf am
|
||||||
|
fokussierten Knoten (WAI-ARIA-Baum-Idiom).
|
||||||
|
|
||||||
|
- **Anfangszustand aus dem Text (§1):** `>` klappt ein. `<` holt seinen
|
||||||
|
Teilbaum hervor, indem die Faltung die Pfad-Ebenen **hinunterwandert**:
|
||||||
|
Jeder eingeklappte Vorfahr öffnet sich, seine übrigen Kinder stehen
|
||||||
|
stattdessen als einzelne eingeklappte Knoten da — sichtbar ist genau der
|
||||||
|
Pfad samt Teilbaum, der Rest bleibt kompakt. Ein `>` **innerhalb** des
|
||||||
|
hervorgeholten Teilbaums bleibt respektiert.
|
||||||
|
- Auch ein **`!!!`-markierter Knoten** (§1) holt sich auf diese Weise hervor —
|
||||||
|
ein Zeigefinger auf etwas Unsichtbares zeigte ins Leere.
|
||||||
|
- Der interaktive Eingriff gilt **je Knoten** (Identität = Label-Pfad, wie bei
|
||||||
|
„Was ist neu?“) und **für die Sitzung**; er wird nicht gespeichert — die
|
||||||
|
dauerhafte Aussage steht im Text (D34). Ein Dokumentwechsel setzt zurück.
|
||||||
|
- Faltung ist **reine Ansicht**: Warnungen aus eingeklappten Teilbäumen werden
|
||||||
|
weiter gemeldet (sie gelten dem Text), und der günstigste Pfad rechnet
|
||||||
|
unverändert über den ganzen Baum — seine Linie zeigt nur die sichtbaren
|
||||||
|
Endknoten.
|
||||||
|
- **Export und Druck folgen der sichtbaren Faltung** (dieselbe Regel wie beim
|
||||||
|
„verworfene einblenden“-Filter): Verborgene Teilbäume fehlen, eingeklappte
|
||||||
|
Knoten behalten die Kennzeichnung „▸ n“ — das Bild behauptet damit keine
|
||||||
|
Vollständigkeit. Das ▾ offener Knoten ist ein Bedienelement und erscheint
|
||||||
|
weder im Export noch im Druck.
|
||||||
|
|
||||||
|
Siehe D38.
|
||||||
|
|
||||||
### Grafikexport des Diagramms
|
### Grafikexport des Diagramms
|
||||||
Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen
|
Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen
|
||||||
+ Text, keine externen Ressourcen) nachgezeichnet. Zwei Icon-Schaltflächen:
|
+ Text, keine externen Ressourcen) nachgezeichnet. Zwei Icon-Schaltflächen:
|
||||||
@@ -646,26 +690,6 @@ ist, damit der gewählte Knoten effektiv fertig werden kann. Gemeinsam
|
|||||||
benötigte Abhängigkeiten werden dabei **nur einmal** gezählt. Genau das macht
|
benötigte Abhängigkeiten werden dabei **nur einmal** gezählt. Genau das macht
|
||||||
die Rechnung schwerer als heute — siehe D34.
|
die Rechnung schwerer als heute — siehe D34.
|
||||||
|
|
||||||
### Ein- und ausklappbare Teilbäume (`>` / `<`)
|
|
||||||
|
|
||||||
- `>` an einem Knoten heißt: **ab hier eingeklappt** — sein Teilbaum ist beim
|
|
||||||
Öffnen des Dokuments zunächst verborgen.
|
|
||||||
- `<` innerhalb eines eingeklappten Bereichs holt einen einzelnen Teilbaum
|
|
||||||
gezielt wieder hervor.
|
|
||||||
- Im Diagramm wird danach **unabhängig vom Text** interaktiv auf- und
|
|
||||||
zugeklappt; die Marken im Text bestimmen nur den Anfangszustand.
|
|
||||||
- **Stellung im Zeilenformat (entschieden):** **hinter** dem Zerlegungszeichen,
|
|
||||||
vor der Statusbox — `- > [x] Backend`. Die Spalte der Zerlegungszeichen
|
|
||||||
bleibt so bündig (an ihr liest das Auge die Hierarchie ab), und ein `>` am
|
|
||||||
Zeilenanfang wäre die Markdown-Blockquote-Konvention. Wurzelknoten (ohne
|
|
||||||
Zeichen) tragen die Marke am Zeilenanfang. Begründung: D34-Nachtrag.
|
|
||||||
- **Grafikexport und Druck (entschieden):** folgen der **sichtbar
|
|
||||||
eingeklappten** Struktur — dieselbe Regel wie beim „verworfene
|
|
||||||
einblenden“-Filter (§9: „Es wird genau die sichtbare Struktur exportiert“).
|
|
||||||
Damit das Bild nicht stillschweigend Vollständigkeit behauptet, wird ein
|
|
||||||
eingeklappter Knoten **gekennzeichnet** (z. B. „▸“ oder die Anzahl der
|
|
||||||
verborgenen Kinder; genaue Form wird beim Bauen festgelegt).
|
|
||||||
|
|
||||||
### Knotenbeschreibungen
|
### Knotenbeschreibungen
|
||||||
|
|
||||||
Erläuternder Text zu einem Knoten, im Diagramm als Tooltip oder Pop-up.
|
Erläuternder Text zu einem Knoten, im Diagramm als Tooltip oder Pop-up.
|
||||||
|
|||||||
+5
-1
@@ -91,10 +91,14 @@ entscheiden, **bevor** Code entsteht.
|
|||||||
**benennen**.
|
**benennen**.
|
||||||
- [ ] Querverbindungen zeichnen (eigene SVG-Ebene, optisch sekundär);
|
- [ ] Querverbindungen zeichnen (eigene SVG-Ebene, optisch sekundär);
|
||||||
bei ausgewähltem Knoten ein-/ausgehende hervorheben.
|
bei ausgewähltem Knoten ein-/ausgehende hervorheben.
|
||||||
- [ ] Faltmarken `>` / `<` parsen; interaktives Auf-/Zuklappen im Diagramm.
|
- [x] Faltmarken `>` / `<` parsen; interaktives Auf-/Zuklappen im Diagramm.
|
||||||
→ Schreibweise und Export-Verhalten entschieden (D34-Nachtrag): Marke
|
→ Schreibweise und Export-Verhalten entschieden (D34-Nachtrag): Marke
|
||||||
hinter dem Zerlegungszeichen (`- > [x] …`), Export/Druck folgen der
|
hinter dem Zerlegungszeichen (`- > [x] …`), Export/Druck folgen der
|
||||||
sichtbar eingeklappten Struktur mit Kennzeichnung am Knoten.
|
sichtbar eingeklappten Struktur mit Kennzeichnung am Knoten.
|
||||||
|
Umgesetzt (D38): `initialCollapsed()` (`<` wandert die Faltung die
|
||||||
|
Pfad-Ebenen hinunter, `!!!` holt sich mit hervor); Falt-Zeichen ▾/„▸ n"
|
||||||
|
+ Tastatur ←/→; Eingriffe je Label-Pfad, nur für die Sitzung; Warnungen
|
||||||
|
aus eingeklappten Teilbäumen bleiben; `tests/fold.test.js`.
|
||||||
- [x] XOR-Gruppe (`=`) parsen (nur mit folgendem Leerraum, Mischung →
|
- [x] XOR-Gruppe (`=`) parsen (nur mit folgendem Leerraum, Mischung →
|
||||||
`mixedGate`), zwei realisierte Alternativen melden.
|
`mixedGate`), zwei realisierte Alternativen melden.
|
||||||
→ Zeichen entschieden: `=` statt `x` (D34-Nachtrag). Umgesetzt (D35):
|
→ Zeichen entschieden: `=` statt `x` (D34-Nachtrag). Umgesetzt (D35):
|
||||||
|
|||||||
@@ -60,9 +60,9 @@
|
|||||||
- [^] Load a document from ?sourceUrl= (S)
|
- [^] Load a document from ?sourceUrl= (S)
|
||||||
- [^] Jump between diagram and text (S)
|
- [^] Jump between diagram and text (S)
|
||||||
- [x] Line numbers in the text editor (XS) %% the warnings name them
|
- [x] Line numbers in the text editor (XS) %% the warnings name them
|
||||||
- [?] Collapsible subtrees (M)
|
- [x] Collapsible subtrees (M)
|
||||||
- [?] Initial state from the text: > collapses, < re-opens (S)
|
- [x] Initial state from the text: > collapses, < re-opens (S)
|
||||||
- [?] Fold and unfold in the diagram, independently of the text (S)
|
- [x] Fold and unfold in the diagram, independently of the text (S)
|
||||||
- [ ] Open and save .werkbaum files (S)
|
- [ ] Open and save .werkbaum files (S)
|
||||||
+ [^] Show what is new since your last visit (S)
|
+ [^] Show what is new since your last visit (S)
|
||||||
+ [?] A pastel colour per person (S)
|
+ [?] A pastel colour per person (S)
|
||||||
|
|||||||
@@ -267,6 +267,19 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
passiert: Zähler stimmte, nichts leuchtete). Vorgehalten wird nur
|
passiert: Zähler stimmte, nichts leuchtete). Vorgehalten wird nur
|
||||||
`freshPrevRoots` (Basis, einmal geparst). Basis je Dokument in `werkbaum-seen`,
|
`freshPrevRoots` (Basis, einmal geparst). Basis je Dokument in `werkbaum-seen`,
|
||||||
fortgeschrieben **erst beim Bestätigen** über `#freshBtn`.
|
fortgeschrieben **erst beim Bestätigen** über `#freshBtn`.
|
||||||
|
- Faltmarken `>`/`<` (SPEC §1/§9, D38): Parser setzt nur `fold` ('>'|'<'|null,
|
||||||
|
Leerraum-Regel); den wirksamen Anfangszustand rechnet `initialCollapsed()`
|
||||||
|
in model.js — `<` (und die Fokusmarke) wandert die Faltung die Pfad-Ebenen
|
||||||
|
**hinunter** statt Vorfahren bloß zu öffnen. `render()` überlagert ihn mit
|
||||||
|
`foldOverrides` (Schlüssel = Label-Pfad via `nodeKeys()`, Sitzung, beim
|
||||||
|
Dokumentwechsel geleert) und übergibt `collapsedSet` an den Renderer; der
|
||||||
|
lässt eingeklappte Kinder **weg** (nicht CSS-verstecken — Export, Messungen
|
||||||
|
und Pfadlinie bleiben so von selbst konsistent), meldet deren Warnungen aber
|
||||||
|
weiter (`walkFolded`, zählt zugleich fürs „▸ n"). Umklappen: Klick aufs
|
||||||
|
`.fold`-Zeichen (preventDefault — es sitzt bei Link-Knoten im `<a>`) oder
|
||||||
|
←/→ am fokussierten Knoten; nach `render()` den Fokus per `data-line`
|
||||||
|
wiederherstellen. Export/Druck: „▸ n" bleibt, das ▾ offener Knoten fällt weg
|
||||||
|
(`stripFold` in `diagramToSvg`, Print-Regel `.node:not(.folded) .fold`).
|
||||||
- Knoten-IDs `#name` (SPEC §1/D36): nur **alleinstehend angesetzt** und nur
|
- Knoten-IDs `#name` (SPEC §1/D36): nur **alleinstehend angesetzt** und nur
|
||||||
der **erste** Treffer der Zeile (kein `/g`!) — weitere `#`-Token bleiben im
|
der **erste** Treffer der Zeile (kein `/g`!) — weitere `#`-Token bleiben im
|
||||||
Label (reservierte Ticket-Referenzen), und `:#a,#b` (künftige Abhängigkeiten)
|
Label (reservierte Ticket-Referenzen), und `:#a,#b` (künftige Abhängigkeiten)
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
Personen mit <code>@name</code> — erscheinen unten rechts am Knoten.
|
Personen mit <code>@name</code> — erscheinen unten rechts am Knoten.
|
||||||
Knoten-ID mit <code>#name</code> — erscheint im Tooltip des Knotens.
|
Knoten-ID mit <code>#name</code> — erscheint im Tooltip des Knotens.
|
||||||
Abhängigkeiten mit <code>:#name,#name</code> — erscheinen im Tooltip.
|
Abhängigkeiten mit <code>:#name,#name</code> — erscheinen im Tooltip.
|
||||||
|
Falten: <code>- > [x] …</code> startet eingeklappt, <code><</code> holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→).
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+86
-12
@@ -1,6 +1,6 @@
|
|||||||
import './style.css';
|
import './style.css';
|
||||||
import { parse } from './parser.js';
|
import { parse } from './parser.js';
|
||||||
import { computeCheapSet, freshProdSet } from './model.js';
|
import { computeCheapSet, freshProdSet, initialCollapsed, nodeKeys } from './model.js';
|
||||||
import { esc, renderTreeHtml } from './render.js';
|
import { esc, renderTreeHtml } from './render.js';
|
||||||
import { formatWarning } from './warnings.js';
|
import { formatWarning } from './warnings.js';
|
||||||
import { padUrls } from './remote.js';
|
import { padUrls } from './remote.js';
|
||||||
@@ -61,6 +61,12 @@ let sourceWarning = null;
|
|||||||
Vergleichsfassung. `freshBaseline` ist der Text, gegen den verglichen wurde;
|
Vergleichsfassung. `freshBaseline` ist der Text, gegen den verglichen wurde;
|
||||||
er wird erst beim Bestätigen fortgeschrieben. */
|
er wird erst beim Bestätigen fortgeschrieben. */
|
||||||
let freshSet = new Set(), freshDocId = null, freshBaseline = null, freshPrevRoots = null;
|
let freshSet = new Set(), freshDocId = null, freshBaseline = null, freshPrevRoots = null;
|
||||||
|
/* Faltung (SPEC §9, D38): `foldOverrides` sind die interaktiven Eingriffe des
|
||||||
|
Nutzers (Schlüssel = Label-Pfad wie bei D28, damit sie das Neu-Parsen bei
|
||||||
|
jedem Tastendruck überleben); sie überlagern den Anfangszustand aus den
|
||||||
|
Textmarken, gelten nur für die Sitzung und fallen beim Dokumentwechsel weg.
|
||||||
|
`foldByLine` ist der Zustand des letzten Renders für Klick/Tastatur. */
|
||||||
|
let foldOverrides = new Map(), foldByLine = new Map();
|
||||||
|
|
||||||
/* ---------- Renderer (Anbindung an den DOM) ----------
|
/* ---------- Renderer (Anbindung an den DOM) ----------
|
||||||
parse -> Wurzeln filtern (verworfene) -> günstigen Pfad markieren ->
|
parse -> Wurzeln filtern (verworfene) -> günstigen Pfad markieren ->
|
||||||
@@ -82,6 +88,7 @@ function render(){
|
|||||||
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();
|
||||||
} else {
|
} else {
|
||||||
const cheapSet = cheapPathOn ? computeCheapSet(roots) : new Set();
|
const cheapSet = cheapPathOn ? computeCheapSet(roots) : new Set();
|
||||||
out.classList.toggle('cheap-on', cheapPathOn);
|
out.classList.toggle('cheap-on', cheapPathOn);
|
||||||
@@ -91,8 +98,21 @@ function render(){
|
|||||||
Objektidentität nie zu (D28). */
|
Objektidentität nie zu (D28). */
|
||||||
freshSet = (freshDocId === activeId && freshPrevRoots)
|
freshSet = (freshDocId === activeId && freshPrevRoots)
|
||||||
? freshProdSet(freshPrevRoots, roots) : new Set();
|
? freshProdSet(freshPrevRoots, roots) : new Set();
|
||||||
|
/* Faltung (D38): Anfangszustand aus den Textmarken (`!!!` holt sich mit
|
||||||
|
hervor), überlagert von den Sitzungs-Eingriffen des Nutzers. Wie bei
|
||||||
|
`freshSet` muss die Menge aus den gerade geparsten Knoten bestehen. */
|
||||||
|
const initFold = initialCollapsed(roots, true);
|
||||||
|
const keys = nodeKeys(roots);
|
||||||
|
const collapsedSet = new Set();
|
||||||
|
foldByLine = new Map();
|
||||||
|
keys.forEach((key, n) => {
|
||||||
|
const ov = foldOverrides.get(key);
|
||||||
|
const collapsed = (ov !== undefined ? ov : initFold.has(n)) && n.children.length > 0;
|
||||||
|
if(collapsed) collapsedSet.add(n);
|
||||||
|
foldByLine.set(n.line, {key, collapsed, canFold: n.children.length > 0});
|
||||||
|
});
|
||||||
const r = renderTreeHtml(roots, {t, showDiscarded, cheapPath: cheapPathOn, cheapSet,
|
const r = renderTreeHtml(roots, {t, showDiscarded, cheapPath: cheapPathOn, cheapSet,
|
||||||
freshSet});
|
freshSet, collapsedSet});
|
||||||
out.innerHTML = r.html;
|
out.innerHTML = r.html;
|
||||||
warnings = warnings.concat(r.warnings);
|
warnings = warnings.concat(r.warnings);
|
||||||
}
|
}
|
||||||
@@ -378,7 +398,11 @@ function diagramToSvg(){
|
|||||||
const dashed = cs.borderTopStyle === 'dashed';
|
const dashed = cs.borderTopStyle === 'dashed';
|
||||||
parts.push(`<rect x="${b.x.toFixed(1)}" y="${b.y.toFixed(1)}" width="${b.w.toFixed(1)}" height="${b.h.toFixed(1)}" rx="8" fill="${cs.backgroundColor}" stroke="${cs.borderTopColor}" stroke-width="${parseFloat(cs.borderTopWidth)||1.5}"${dashed?' stroke-dasharray="4 3"':''}/>`);
|
parts.push(`<rect x="${b.x.toFixed(1)}" y="${b.y.toFixed(1)}" width="${b.w.toFixed(1)}" height="${b.h.toFixed(1)}" rx="8" fill="${cs.backgroundColor}" stroke="${cs.borderTopColor}" stroke-width="${parseFloat(cs.borderTopWidth)||1.5}"${dashed?' stroke-dasharray="4 3"':''}/>`);
|
||||||
const clone = node.cloneNode(true);
|
const clone = node.cloneNode(true);
|
||||||
clone.querySelectorAll('.size,.tags,.ext,.risk').forEach(e => e.remove());
|
/* Das „▸ n"-Kennzeichen eingeklappter Knoten gehört in den Export (SPEC
|
||||||
|
§9/D38 — das Bild darf keine Vollständigkeit behaupten); das ▾ offener
|
||||||
|
Knoten ist Bedienelement und fällt weg. */
|
||||||
|
const stripFold = node.classList.contains('folded') ? '' : ',.fold';
|
||||||
|
clone.querySelectorAll('.size,.tags,.ext,.risk' + stripFold).forEach(e => e.remove());
|
||||||
const label = clone.textContent.replace(/\s+/g,' ').trim();
|
const label = clone.textContent.replace(/\s+/g,' ').trim();
|
||||||
const deco = cs.textDecorationLine.includes('line-through') ? ' text-decoration="line-through"' : '';
|
const deco = cs.textDecorationLine.includes('line-through') ? ' text-decoration="line-through"' : '';
|
||||||
parts.push(`<text x="${b.cx.toFixed(1)}" y="${(b.cy+5).toFixed(1)}" text-anchor="middle" fill="${cs.color}" font-size="14" font-weight="${cs.fontWeight}"${deco}>${esc(label)}</text>`);
|
parts.push(`<text x="${b.cx.toFixed(1)}" y="${(b.cy+5).toFixed(1)}" text-anchor="middle" fill="${cs.color}" font-size="14" font-weight="${cs.fontWeight}"${deco}>${esc(label)}</text>`);
|
||||||
@@ -685,6 +709,29 @@ function nodeFromEvent(e){
|
|||||||
return el && out.contains(el) ? el : null;
|
return el && out.contains(el) ? el : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Faltung umklappen (SPEC §9, D38). Der Eingriff überlagert den Anfangszustand
|
||||||
|
aus dem Text; nach dem Neubau bekommt derselbe Knoten den Fokus zurück,
|
||||||
|
sonst risse die Tastaturbedienung ab (das alte Element ist weg). */
|
||||||
|
function toggleFold(el){
|
||||||
|
const st = foldByLine.get(+el.dataset.line);
|
||||||
|
if(!st || !st.canFold) return;
|
||||||
|
foldOverrides.set(st.key, !st.collapsed);
|
||||||
|
render();
|
||||||
|
const again = out.querySelector('.node[data-line="' + el.dataset.line + '"]');
|
||||||
|
if(again) again.focus({preventScroll: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Klick auf das Falt-Zeichen ▾/▸ klappt um. preventDefault, weil das Zeichen
|
||||||
|
bei Link-Knoten IM <a> sitzt — sonst öffnete der Klick zusätzlich die URL. */
|
||||||
|
out.addEventListener('click', e => {
|
||||||
|
const f = e.target && e.target.closest ? e.target.closest('.fold') : null;
|
||||||
|
if(!f || !out.contains(f) || e.altKey) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const el = f.closest('.node[data-line]');
|
||||||
|
if(el) toggleFold(el);
|
||||||
|
});
|
||||||
|
|
||||||
out.addEventListener('click', e => {
|
out.addEventListener('click', e => {
|
||||||
if(!e.altKey) return;
|
if(!e.altKey) return;
|
||||||
const el = nodeFromEvent(e);
|
const el = nodeFromEvent(e);
|
||||||
@@ -705,6 +752,20 @@ out.addEventListener('keydown', e => {
|
|||||||
jumpToLine(+el.dataset.line);
|
jumpToLine(+el.dataset.line);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Tastatur-Faltung (SPEC §9, D38): ← klappt zu, → klappt auf — das
|
||||||
|
WAI-ARIA-Baum-Idiom. Nur ohne Modifier, damit nichts anderes kollidiert. */
|
||||||
|
out.addEventListener('keydown', e => {
|
||||||
|
if(e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
||||||
|
if(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
||||||
|
const el = nodeFromEvent(e);
|
||||||
|
if(!el) return;
|
||||||
|
const st = foldByLine.get(+el.dataset.line);
|
||||||
|
if(!st || !st.canFold) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const want = e.key === 'ArrowLeft';
|
||||||
|
if(st.collapsed !== want) toggleFold(el);
|
||||||
|
});
|
||||||
|
|
||||||
/* Mobil gibt es kein Alt: langer Druck (500 ms) auf einen Knoten springt.
|
/* Mobil gibt es kein Alt: langer Druck (500 ms) auf einen Knoten springt.
|
||||||
Der Sprung passiert erst beim LOSLASSEN: `focus()` aus einem Timer heraus
|
Der Sprung passiert erst beim LOSLASSEN: `focus()` aus einem Timer heraus
|
||||||
gilt in mobilen Browsern nicht als Nutzergeste — der Fokus fiel sofort wieder
|
gilt in mobilen Browsern nicht als Nutzergeste — der Fokus fiel sofort wieder
|
||||||
@@ -1109,7 +1170,7 @@ const I18N = {
|
|||||||
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
||||||
sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"„{url}“ hat innerhalb von {seconds} s nicht geantwortet — der Abruf wurde abgebrochen. Etherpad begrenzt, wie oft der Export geholt werden darf (serienmäßig 10-mal pro 90 s); warte einen Moment und lade dann erneut.",
|
sourceTimeoutWarn:"„{url}“ hat innerhalb von {seconds} s nicht geantwortet — der Abruf wurde abgebrochen. Etherpad begrenzt, wie oft der Export geholt werden darf (serienmäßig 10-mal pro 90 s); warte einen Moment und lade dann erneut.",
|
||||||
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: M (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
|
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: M (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yFolded:"eingeklappt, {n} verborgen", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
|
||||||
hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.",
|
hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.",
|
||||||
hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
|
hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
|
||||||
hint_xor:"Alternative, genau eine",
|
hint_xor:"Alternative, genau eine",
|
||||||
@@ -1123,6 +1184,7 @@ const I18N = {
|
|||||||
hint_people:"Personen mit @name — erscheinen unten rechts am Knoten.",
|
hint_people:"Personen mit @name — erscheinen unten rechts am Knoten.",
|
||||||
hint_id:"Knoten-ID mit #name — erscheint im Tooltip des Knotens.",
|
hint_id:"Knoten-ID mit #name — erscheint im Tooltip des Knotens.",
|
||||||
hint_deps:"Abhängigkeiten mit :#name,#name — erscheinen im Tooltip.",
|
hint_deps:"Abhängigkeiten mit :#name,#name — erscheinen im Tooltip.",
|
||||||
|
hint_fold:"Falten: - > [x] … startet eingeklappt, < holt hervor; ▾/▸ am Knoten klappt um (Tastatur: ←/→).",
|
||||||
hint_jump:"Alt+Klick auf einen Knoten (mobil: langer Druck) springt zur zugehörigen Textzeile; Alt+Klick im Text holt den Knoten ins Bild."
|
hint_jump:"Alt+Klick auf einen Knoten (mobil: langer Druck) springt zur zugehörigen Textzeile; Alt+Klick im Text holt den Knoten ins Bild."
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
@@ -1179,7 +1241,7 @@ const I18N = {
|
|||||||
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
||||||
sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"“{url}” did not answer within {seconds} s — the request was aborted. Etherpad limits how often the export may be fetched (10 times per 90 s by default); wait a moment, then reload.",
|
sourceTimeoutWarn:"“{url}” did not answer within {seconds} s — the request was aborted. Etherpad limits how often the export may be fetched (10 times per 90 s by default); wait a moment, then reload.",
|
||||||
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: M (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
|
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: M (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yFolded:"collapsed, {n} hidden", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
|
||||||
hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.",
|
hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.",
|
||||||
hint_all:"sub-task, all required", hint_any:"alternative, choose one",
|
hint_all:"sub-task, all required", hint_any:"alternative, choose one",
|
||||||
hint_xor:"alternative, exactly one",
|
hint_xor:"alternative, exactly one",
|
||||||
@@ -1193,6 +1255,7 @@ const I18N = {
|
|||||||
hint_people:"People with @name — shown at the bottom-right of the node.",
|
hint_people:"People with @name — shown at the bottom-right of the node.",
|
||||||
hint_id:"Node ID with #name — shown in the node's tooltip.",
|
hint_id:"Node ID with #name — shown in the node's tooltip.",
|
||||||
hint_deps:"Dependencies with :#name,#name — shown in the tooltip.",
|
hint_deps:"Dependencies with :#name,#name — shown in the tooltip.",
|
||||||
|
hint_fold:"Folding: - > [x] … starts collapsed, < brings it back; ▾/▸ on a node toggles (keyboard: ←/→).",
|
||||||
hint_jump:"Alt+click a node (long press on touch) jumps to its line in the text; Alt+click in the text brings the node into view."
|
hint_jump:"Alt+click a node (long press on touch) jumps to its line in the text; Alt+click in the text brings the node into view."
|
||||||
},
|
},
|
||||||
es: {
|
es: {
|
||||||
@@ -1249,7 +1312,7 @@ const I18N = {
|
|||||||
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.",
|
||||||
sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"«{url}» no respondió en {seconds} s — se canceló la petición. Etherpad limita la frecuencia de descarga del export (10 veces por 90 s de forma predeterminada); espera un momento y vuelve a cargar.",
|
sourceTimeoutWarn:"«{url}» no respondió en {seconds} s — se canceló la petición. Etherpad limita la frecuencia de descarga del export (10 veces por 90 s de forma predeterminada); espera un momento y vuelve a cargar.",
|
||||||
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: M (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
|
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: M (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yFolded:"plegado, {n} ocultos", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
|
||||||
hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.",
|
hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.",
|
||||||
hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una",
|
hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una",
|
||||||
hint_xor:"alternativa, exactamente una",
|
hint_xor:"alternativa, exactamente una",
|
||||||
@@ -1263,6 +1326,7 @@ const I18N = {
|
|||||||
hint_people:"Personas con @nombre — aparecen abajo a la derecha del nodo.",
|
hint_people:"Personas con @nombre — aparecen abajo a la derecha del nodo.",
|
||||||
hint_id:"ID de nodo con #nombre — visible en el tooltip del nodo.",
|
hint_id:"ID de nodo con #nombre — visible en el tooltip del nodo.",
|
||||||
hint_deps:"Dependencias con :#nombre,#nombre — visibles en el tooltip.",
|
hint_deps:"Dependencias con :#nombre,#nombre — visibles en el tooltip.",
|
||||||
|
hint_fold:"Plegado: - > [x] … empieza plegado, < lo recupera; ▾/▸ en el nodo alterna (teclado: ←/→).",
|
||||||
hint_jump:"Alt+clic en un nodo (pulsación larga en táctil) salta a su línea en el texto; Alt+clic en el texto trae el nodo a la vista."
|
hint_jump:"Alt+clic en un nodo (pulsación larga en táctil) salta a su línea en el texto; Alt+clic en el texto trae el nodo a la vista."
|
||||||
},
|
},
|
||||||
fr: {
|
fr: {
|
||||||
@@ -1319,7 +1383,7 @@ const I18N = {
|
|||||||
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
||||||
sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"« {url} » n’a pas répondu en {seconds} s — la requête a été interrompue. Etherpad limite la fréquence de récupération de l’export (10 fois par 90 s par défaut) ; attends un instant, puis recharge.",
|
sourceTimeoutWarn:"« {url} » n’a pas répondu en {seconds} s — la requête a été interrompue. Etherpad limite la fréquence de récupération de l’export (10 fois par 90 s par défaut) ; attends un instant, puis recharge.",
|
||||||
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : M (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
|
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : M (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yFolded:"replié, {n} masqués", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
|
||||||
hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.",
|
hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.",
|
||||||
hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une",
|
hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une",
|
||||||
hint_xor:"alternative, exactement une",
|
hint_xor:"alternative, exactement une",
|
||||||
@@ -1333,6 +1397,7 @@ const I18N = {
|
|||||||
hint_people:"Personnes avec @nom — affichées en bas à droite du nœud.",
|
hint_people:"Personnes avec @nom — affichées en bas à droite du nœud.",
|
||||||
hint_id:"ID de nœud avec #nom — visible dans l’infobulle du nœud.",
|
hint_id:"ID de nœud avec #nom — visible dans l’infobulle du nœud.",
|
||||||
hint_deps:"Dépendances avec :#nom,#nom — visibles dans l’infobulle.",
|
hint_deps:"Dépendances avec :#nom,#nom — visibles dans l’infobulle.",
|
||||||
|
hint_fold:"Pliage : - > [x] … démarre replié, < le fait ressortir ; ▾/▸ sur le nœud bascule (clavier : ←/→).",
|
||||||
hint_jump:"Alt+clic sur un nœud (appui long sur tactile) saute à sa ligne dans le texte ; Alt+clic dans le texte amène le nœud à l’écran."
|
hint_jump:"Alt+clic sur un nœud (appui long sur tactile) saute à sa ligne dans le texte ; Alt+clic dans le texte amène le nœud à l’écran."
|
||||||
},
|
},
|
||||||
pl: {
|
pl: {
|
||||||
@@ -1389,7 +1454,7 @@ const I18N = {
|
|||||||
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
||||||
sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"„{url}” nie odpowiedział w ciągu {seconds} s — żądanie przerwano. Etherpad ogranicza częstość pobierania eksportu (domyślnie 10 razy na 90 s); odczekaj chwilę i wczytaj ponownie.",
|
sourceTimeoutWarn:"„{url}” nie odpowiedział w ciągu {seconds} s — żądanie przerwano. Etherpad ogranicza częstość pobierania eksportu (domyślnie 10 razy na 90 s); odczekaj chwilę i wczytaj ponownie.",
|
||||||
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: M (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem",
|
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: M (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yFolded:"zwinięte, ukrytych: {n}", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem",
|
||||||
hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.",
|
hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.",
|
||||||
hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
|
hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
|
||||||
hint_xor:"alternatywa, dokładnie jedna",
|
hint_xor:"alternatywa, dokładnie jedna",
|
||||||
@@ -1403,6 +1468,7 @@ const I18N = {
|
|||||||
hint_people:"Osoby z @nazwa — pokazywane w prawym dolnym rogu węzła.",
|
hint_people:"Osoby z @nazwa — pokazywane w prawym dolnym rogu węzła.",
|
||||||
hint_id:"ID węzła przez #nazwa — widoczne w podpowiedzi węzła.",
|
hint_id:"ID węzła przez #nazwa — widoczne w podpowiedzi węzła.",
|
||||||
hint_deps:"Zależności przez :#nazwa,#nazwa — widoczne w podpowiedzi.",
|
hint_deps:"Zależności przez :#nazwa,#nazwa — widoczne w podpowiedzi.",
|
||||||
|
hint_fold:"Zwijanie: - > [x] … zaczyna zwinięte, < przywraca; ▾/▸ na węźle przełącza (klawiatura: ←/→).",
|
||||||
hint_jump:"Alt+kliknięcie węzła (długie naciśnięcie na dotyku) przechodzi do jego wiersza w tekście; Alt+kliknięcie w tekście pokazuje węzeł na diagramie."
|
hint_jump:"Alt+kliknięcie węzła (długie naciśnięcie na dotyku) przechodzi do jego wiersza w tekście; Alt+kliknięcie w tekście pokazuje węzeł na diagramie."
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
@@ -1459,7 +1525,7 @@ const I18N = {
|
|||||||
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
||||||
sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"«{url}» не ответил за {seconds} с — запрос прерван. Etherpad ограничивает частоту загрузки экспорта (по умолчанию 10 раз за 90 с); подождите немного и обновите снова.",
|
sourceTimeoutWarn:"«{url}» не ответил за {seconds} с — запрос прерван. Etherpad ограничивает частоту загрузки экспорта (по умолчанию 10 раз за 90 с); подождите немного и обновите снова.",
|
||||||
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
|
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yFolded:"свёрнуто, скрыто: {n}", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
|
||||||
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
||||||
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
||||||
hint_xor:"альтернатива, ровно одна",
|
hint_xor:"альтернатива, ровно одна",
|
||||||
@@ -1473,6 +1539,7 @@ const I18N = {
|
|||||||
hint_people:"Люди через @имя — показываются справа внизу узла.",
|
hint_people:"Люди через @имя — показываются справа внизу узла.",
|
||||||
hint_id:"ID узла через #имя — виден во всплывающей подсказке узла.",
|
hint_id:"ID узла через #имя — виден во всплывающей подсказке узла.",
|
||||||
hint_deps:"Зависимости через :#имя,#имя — видны в подсказке.",
|
hint_deps:"Зависимости через :#имя,#имя — видны в подсказке.",
|
||||||
|
hint_fold:"Сворачивание: - > [x] … открывается свёрнутым, < возвращает; ▾/▸ на узле переключает (клавиши: ←/→).",
|
||||||
hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме."
|
hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме."
|
||||||
},
|
},
|
||||||
hi: {
|
hi: {
|
||||||
@@ -1529,7 +1596,7 @@ const I18N = {
|
|||||||
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
||||||
sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।",
|
sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।",
|
||||||
sourceTimeoutWarn:"„{url}“ ने {seconds} स॰ में उत्तर नहीं दिया — अनुरोध रद्द कर दिया गया। Etherpad सीमित करता है कि एक्सपोर्ट कितनी बार लिया जा सके (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार); कुछ क्षण रुकें, फिर दोबारा लोड करें।",
|
sourceTimeoutWarn:"„{url}“ ने {seconds} स॰ में उत्तर नहीं दिया — अनुरोध रद्द कर दिया गया। Etherpad सीमित करता है कि एक्सपोर्ट कितनी बार लिया जा सके (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार); कुछ क्षण रुकें, फिर दोबारा लोड करें।",
|
||||||
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
|
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yFolded:"समेटा हुआ, {n} छिपे", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
|
||||||
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
||||||
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
||||||
hint_xor:"विकल्प, ठीक एक",
|
hint_xor:"विकल्प, ठीक एक",
|
||||||
@@ -1543,6 +1610,7 @@ const I18N = {
|
|||||||
hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।",
|
hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।",
|
||||||
hint_id:"#नाम से नोड आईडी — नोड के टूलटिप में दिखती है।",
|
hint_id:"#नाम से नोड आईडी — नोड के टूलटिप में दिखती है।",
|
||||||
hint_deps:":#नाम,#नाम से निर्भरताएँ — टूलटिप में दिखती हैं।",
|
hint_deps:":#नाम,#नाम से निर्भरताएँ — टूलटिप में दिखती हैं।",
|
||||||
|
hint_fold:"फ़ोल्डिंग: - > [x] … समेटा हुआ खुलता है, < वापस लाता है; नोड पर ▾/▸ टॉगल करता है (कीबोर्ड: ←/→)।",
|
||||||
hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है।"
|
hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है।"
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
@@ -1599,7 +1667,7 @@ const I18N = {
|
|||||||
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
||||||
sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORS(Access-Control-Allow-Origin)。",
|
sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORS(Access-Control-Allow-Origin)。",
|
||||||
sourceTimeoutWarn:"“{url}” 在 {seconds} 秒内没有响应 — 请求已中止。Etherpad 会限制导出的获取频率(默认每 90 秒 10 次);请稍候再重新加载。",
|
sourceTimeoutWarn:"“{url}” 在 {seconds} 秒内没有响应 — 请求已中止。Etherpad 会限制导出的获取频率(默认每 90 秒 10 次);请稍候再重新加载。",
|
||||||
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yId:"ID:#{id}", a11yDeps:"依赖:{ids}", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
|
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yId:"ID:#{id}", a11yDeps:"依赖:{ids}", a11yFolded:"已折叠,隐藏 {n} 项", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
|
||||||
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
||||||
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
||||||
hint_xor:"备选项,恰好一个",
|
hint_xor:"备选项,恰好一个",
|
||||||
@@ -1613,6 +1681,7 @@ const I18N = {
|
|||||||
hint_people:"用 @姓名 表示人员——显示在节点右下角。",
|
hint_people:"用 @姓名 表示人员——显示在节点右下角。",
|
||||||
hint_id:"用 #名称 指定节点 ID——显示在节点提示中。",
|
hint_id:"用 #名称 指定节点 ID——显示在节点提示中。",
|
||||||
hint_deps:"用 :#名称,#名称 表示依赖——显示在提示中。",
|
hint_deps:"用 :#名称,#名称 表示依赖——显示在提示中。",
|
||||||
|
hint_fold:"折叠:- > [x] … 打开时即折叠,< 将其展开;节点上的 ▾/▸ 切换(键盘:←/→)。",
|
||||||
hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野。"
|
hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野。"
|
||||||
},
|
},
|
||||||
ja: {
|
ja: {
|
||||||
@@ -1669,7 +1738,7 @@ const I18N = {
|
|||||||
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
||||||
sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORS(Access-Control-Allow-Origin)を許可する必要があります。",
|
sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORS(Access-Control-Allow-Origin)を許可する必要があります。",
|
||||||
sourceTimeoutWarn:"「{url}」が {seconds} 秒以内に応答しませんでした — 要求を中止しました。Etherpad はエクスポートの取得回数を制限します(既定で 90 秒あたり 10 回)。少し待ってから再読み込みしてください。",
|
sourceTimeoutWarn:"「{url}」が {seconds} 秒以内に応答しませんでした — 要求を中止しました。Etherpad はエクスポートの取得回数を制限します(既定で 90 秒あたり 10 回)。少し待ってから再読み込みしてください。",
|
||||||
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
|
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yFolded:"折りたたみ中、{n} 件非表示", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
|
||||||
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
|
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
|
||||||
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
|
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
|
||||||
hint_xor:"選択肢、ちょうど1つ",
|
hint_xor:"選択肢、ちょうど1つ",
|
||||||
@@ -1683,6 +1752,7 @@ const I18N = {
|
|||||||
hint_people:"@名前 で担当者 — ノードの右下に表示されます。",
|
hint_people:"@名前 で担当者 — ノードの右下に表示されます。",
|
||||||
hint_id:"#名前 でノード ID — ノードのツールチップに表示されます。",
|
hint_id:"#名前 でノード ID — ノードのツールチップに表示されます。",
|
||||||
hint_deps:":#名前,#名前 で依存関係 — ツールチップに表示されます。",
|
hint_deps:":#名前,#名前 で依存関係 — ツールチップに表示されます。",
|
||||||
|
hint_fold:"折りたたみ:- > [x] … は折りたたんだ状態で開き、< は呼び戻します。ノードの ▾/▸ で切替(キー:←/→)。",
|
||||||
hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。"
|
hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1717,6 +1787,7 @@ function buildHint(){
|
|||||||
${esc(t('hint_people'))}
|
${esc(t('hint_people'))}
|
||||||
${esc(t('hint_id'))}
|
${esc(t('hint_id'))}
|
||||||
${esc(t('hint_deps'))}
|
${esc(t('hint_deps'))}
|
||||||
|
${esc(t('hint_fold'))}
|
||||||
<code>!!!</code> ${esc(t('hint_focus'))}
|
<code>!!!</code> ${esc(t('hint_focus'))}
|
||||||
<div class="hint-op">${esc(t('hint_jump'))}</div>`;
|
<div class="hint-op">${esc(t('hint_jump'))}</div>`;
|
||||||
}
|
}
|
||||||
@@ -2076,6 +2147,7 @@ function switchDoc(id){
|
|||||||
if(id === activeId) return;
|
if(id === activeId) return;
|
||||||
flushActive();
|
flushActive();
|
||||||
activeId = id;
|
activeId = id;
|
||||||
|
foldOverrides.clear(); /* Falt-Eingriffe gelten je Dokument-Sitzung (D38) */
|
||||||
loadActiveIntoEditor();
|
loadActiveIntoEditor();
|
||||||
persistDocs();
|
persistDocs();
|
||||||
}
|
}
|
||||||
@@ -2084,6 +2156,7 @@ function newDoc(){
|
|||||||
const d = { id: uid(), name: uniqueName(t('docNewName')), text: '' };
|
const d = { id: uid(), name: uniqueName(t('docNewName')), text: '' };
|
||||||
docs.push(d);
|
docs.push(d);
|
||||||
activeId = d.id;
|
activeId = d.id;
|
||||||
|
foldOverrides.clear();
|
||||||
loadActiveIntoEditor();
|
loadActiveIntoEditor();
|
||||||
persistDocs();
|
persistDocs();
|
||||||
keyboardOnJump(false); /* neues, leeres Dokument = tippen ist gemeint */
|
keyboardOnJump(false); /* neues, leeres Dokument = tippen ist gemeint */
|
||||||
@@ -2112,6 +2185,7 @@ function deleteDoc(){
|
|||||||
docs = docs.filter(x => x.id !== d.id);
|
docs = docs.filter(x => x.id !== d.id);
|
||||||
if(!docs.length) docs = [{ id: EXAMPLE_ID, name: EXAMPLE_NAME, text: INITIAL }];
|
if(!docs.length) docs = [{ id: EXAMPLE_ID, name: EXAMPLE_NAME, text: INITIAL }];
|
||||||
activeId = docs[0].id;
|
activeId = docs[0].id;
|
||||||
|
foldOverrides.clear();
|
||||||
loadActiveIntoEditor();
|
loadActiveIntoEditor();
|
||||||
persistDocs();
|
persistDocs();
|
||||||
closeDocMenu();
|
closeDocMenu();
|
||||||
|
|||||||
@@ -100,6 +100,46 @@ function walkKeys(nodes, parentKey, fn){
|
|||||||
walkKeys(n.children, key, fn);
|
walkKeys(n.children, key, fn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Knoten -> stabiler Schlüssel (Label-Pfad) über den ganzen Baum. Dieselbe
|
||||||
|
Identität wie bei „Was ist neu?" — sie überlebt Umsortieren und Neu-Parsen;
|
||||||
|
genutzt für die interaktiven Falt-Eingriffe (D38). */
|
||||||
|
export function nodeKeys(roots){
|
||||||
|
const map = new Map();
|
||||||
|
walkKeys(roots, '', (key, n) => map.set(n, key));
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Faltmarken (SPEC §1/§9, D38) ----------
|
||||||
|
Anfangszustand der Faltung aus den Textmarken: `>` klappt den Knoten ein.
|
||||||
|
`<` (und mit `rescueFocus` auch die Fokusmarke `!!!`) holt den eigenen
|
||||||
|
Teilbaum hervor, indem die Faltung die Pfad-Ebenen HINUNTERWANDERT: Jeder
|
||||||
|
eingeklappte Vorfahr wird geöffnet, seine Nicht-Pfad-Kinder werden
|
||||||
|
stattdessen eingeklappt. Sichtbar ist genau der Pfad samt Teilbaum, die
|
||||||
|
Geschwister stehen als einzelne eingeklappte Knoten da — und jede
|
||||||
|
gezeichnete Kante bleibt eine echte. Ein `>` innerhalb des hervorgeholten
|
||||||
|
Teilbaums bleibt respektiert. */
|
||||||
|
export function initialCollapsed(roots, rescueFocus){
|
||||||
|
const set = new Set();
|
||||||
|
const paths = [];
|
||||||
|
const walk = (n, path) => {
|
||||||
|
const p = path.concat(n);
|
||||||
|
if(n.fold === '>') set.add(n);
|
||||||
|
if(n.fold === '<' || (rescueFocus && n.focus)) paths.push(p);
|
||||||
|
n.children.forEach(c => walk(c, p));
|
||||||
|
};
|
||||||
|
roots.forEach(r => walk(r, []));
|
||||||
|
for(const p of paths){
|
||||||
|
for(let i = 0; i < p.length - 1; i++){
|
||||||
|
if(!set.has(p[i])) continue;
|
||||||
|
set.delete(p[i]);
|
||||||
|
for(const c of p[i].children)
|
||||||
|
if(c !== p[i+1] && c.children.length) set.add(c);
|
||||||
|
}
|
||||||
|
set.delete(p[p.length - 1]); /* der geholte Knoten selbst ist offen */
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
/* 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();
|
||||||
|
|||||||
+13
-7
@@ -25,8 +25,10 @@ export const STATUS_BY_CODE = {
|
|||||||
const REALIZED = new Set(['arbeit', 'durchstich', 'fertig', 'prod']);
|
const REALIZED = new Set(['arbeit', 'durchstich', 'fertig', 'prod']);
|
||||||
|
|
||||||
/* Parst den Notationstext zu { roots, warnings }.
|
/* Parst den Notationstext zu { roots, warnings }.
|
||||||
Jeder Knoten: {label, type:'and'|'or'|'xor', optional, status, url, size,
|
Jeder Knoten: {label, type:'and'|'or'|'xor', optional, fold, status, url,
|
||||||
tags, id, deps, focus, children, line}.
|
size, tags, id, deps, focus, children, line}.
|
||||||
|
`fold` ('>'|'<'|null, SPEC §1/D38) ist nur der ANFANGSZUSTAND der Faltung —
|
||||||
|
den wirksamen Zustand rechnet `initialCollapsed()` in model.js.
|
||||||
`deps` sind ID-Strings, keine Knoten-Referenzen — aufgelöst wird erst beim
|
`deps` sind ID-Strings, keine Knoten-Referenzen — aufgelöst wird erst beim
|
||||||
Konsumenten (D37); der Parser prüft nur die Existenz (`unknownDep`).
|
Konsumenten (D37); der Parser prüft nur die Existenz (`unknownDep`).
|
||||||
`type` ist das Gate der Geschwistergruppe, `optional` (Zeichen `+`, SPEC §3)
|
`type` ist das Gate der Geschwistergruppe, `optional` (Zeichen `+`, SPEC §3)
|
||||||
@@ -51,14 +53,18 @@ export function parse(text){
|
|||||||
Statusposition. Gültige Codes -> Status; unbekannte -> Warnung + neutral
|
Statusposition. Gültige Codes -> Status; unbekannte -> Warnung + neutral
|
||||||
(fehlertolerant: die Zeile geht nicht verloren). */
|
(fehlertolerant: die Zeile geht nicht verloren). */
|
||||||
/* `=` (XOR, SPEC §3) nur mit folgendem Leerraum — die Leerraum-Regel hält
|
/* `=` (XOR, SPEC §3) nur mit folgendem Leerraum — die Leerraum-Regel hält
|
||||||
Labels wie `=SUMME(A1:B2)` heraus; `-`/`+`/`|` bleiben wie bisher. */
|
Labels wie `=SUMME(A1:B2)` heraus; `-`/`+`/`|` bleiben wie bisher.
|
||||||
const m = raw.match(/^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:\[([^\]])\]\s*)?(.*)$/);
|
Die Faltmarke `>`/`<` (SPEC §1, D38) steht zwischen Zeichen und
|
||||||
|
Statusbox (bei Wurzeln am Zeilenanfang), ebenfalls nur mit folgendem
|
||||||
|
Leerraum — `- >Achtung` bleibt ein Label. */
|
||||||
|
const m = raw.match(/^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:([><])(?=[ \t])\s*)?(?:\[([^\]])\]\s*)?(.*)$/);
|
||||||
const width = m[1].replace(/\t/g,' ').length;
|
const width = m[1].replace(/\t/g,' ').length;
|
||||||
const type = m[2] === '|' ? 'or' : m[2] === '=' ? 'xor' : 'and';
|
const type = m[2] === '|' ? 'or' : m[2] === '=' ? 'xor' : 'and';
|
||||||
const optional = m[2] === '+';
|
const optional = m[2] === '+';
|
||||||
const boxChar = m[3]; // undefined, wenn keine Statusbox
|
const fold = m[3] || null;
|
||||||
|
const boxChar = m[4]; // undefined, wenn keine Statusbox
|
||||||
|
|
||||||
let rest = m[4], url = null, size = null;
|
let rest = m[5], url = null, size = null;
|
||||||
const tags = [];
|
const tags = [];
|
||||||
rest = rest.replace(/https?:\/\/\S+/i, s => { url = s; return ''; });
|
rest = rest.replace(/https?:\/\/\S+/i, s => { url = s; return ''; });
|
||||||
rest = rest.replace(/\((XXL|XS|XL|S|M|L)\)/i, (s, g) => { size = g.toUpperCase(); return ''; });
|
rest = rest.replace(/\((XXL|XS|XL|S|M|L)\)/i, (s, g) => { size = g.toUpperCase(); return ''; });
|
||||||
@@ -102,7 +108,7 @@ export function parse(text){
|
|||||||
while(stack.length > 1 && stack[stack.length-1].width >= width) stack.pop();
|
while(stack.length > 1 && stack[stack.length-1].width >= width) stack.pop();
|
||||||
const parent = stack[stack.length-1].node;
|
const parent = stack[stack.length-1].node;
|
||||||
|
|
||||||
const node = {label, type, optional, status, url, size, tags, id, deps, focus, children:[], line:i+1};
|
const node = {label, type, optional, fold, status, url, size, tags, id, deps, focus, children:[], line:i+1};
|
||||||
parent.children.push(node);
|
parent.children.push(node);
|
||||||
stack.push({node, width});
|
stack.push({node, width});
|
||||||
});
|
});
|
||||||
|
|||||||
+58
-25
@@ -10,6 +10,7 @@
|
|||||||
cheapPath, // günstigster Pfad aktiv? (steuert das implizite M-Badge)
|
cheapPath, // günstigster Pfad aktiv? (steuert das implizite M-Badge)
|
||||||
cheapSet, // Set der nötigen Knoten (leer, wenn Pfad aus)
|
cheapSet, // Set der nötigen Knoten (leer, wenn Pfad aus)
|
||||||
freshSet, // optional: Knoten, die neu in Produktion sind (D28)
|
freshSet, // optional: Knoten, die neu in Produktion sind (D28)
|
||||||
|
collapsedSet, // optional: eingeklappte Knoten (Faltung, SPEC §9/D38)
|
||||||
} */
|
} */
|
||||||
|
|
||||||
import { gateOf, needsBreakdown, visibleChildren, cheapCls } from './model.js';
|
import { gateOf, needsBreakdown, visibleChildren, cheapCls } from './model.js';
|
||||||
@@ -44,9 +45,12 @@ function attr(s){ return esc(String(s)).replace(/"/g,'"'); }
|
|||||||
Link. Die visuellen Badges (Größe, Tags, ↗) sind aria-hidden — ihre
|
Link. Die visuellen Badges (Größe, Tags, ↗) sind aria-hidden — ihre
|
||||||
Information steckt hier, sonst würde der Screenreader Kryptisches („M",
|
Information steckt hier, sonst würde der Screenreader Kryptisches („M",
|
||||||
„anna", „↗") vorlesen. */
|
„anna", „↗") vorlesen. */
|
||||||
function nodeAria(n, opts){
|
function nodeAria(n, opts, fold){
|
||||||
const { t, cheapPath } = opts;
|
const { t, cheapPath } = opts;
|
||||||
const parts = [n.label];
|
const parts = [n.label];
|
||||||
|
/* Eingeklappt (SPEC §9/D38): das ▾/▸-Zeichen ist aria-hidden — ohne diese
|
||||||
|
Ansage wüsste ein Screenreader nicht, dass hier etwas verborgen ist. */
|
||||||
|
if(fold && fold.collapsed) parts.push(t('a11yFolded', {n: fold.count}));
|
||||||
if(n.status) parts.push(t('a11yStatus', {status: t('st_' + n.status.key)}));
|
if(n.status) parts.push(t('a11yStatus', {status: t('st_' + n.status.key)}));
|
||||||
if(n.size) parts.push(t('a11ySize', {size: n.size}));
|
if(n.size) parts.push(t('a11ySize', {size: n.size}));
|
||||||
else if(cheapPath) parts.push(t('a11ySizeImplicit'));
|
else if(cheapPath) parts.push(t('a11ySizeImplicit'));
|
||||||
@@ -65,10 +69,11 @@ function nodeAria(n, opts){
|
|||||||
return parts.join(', ');
|
return parts.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeHtml(n, extra, opts){
|
function nodeHtml(n, extra, opts, fold){
|
||||||
const { t, cheapPath } = opts;
|
const { t, cheapPath } = opts;
|
||||||
const need = needsBreakdown(n);
|
const need = needsBreakdown(n);
|
||||||
const cls = ['node', extra || '', n.status ? 'st-' + n.status.key : '']
|
const cls = ['node', extra || '', fold && fold.collapsed ? 'folded' : '',
|
||||||
|
n.status ? 'st-' + n.status.key : '']
|
||||||
.filter(Boolean).join(' ');
|
.filter(Boolean).join(' ');
|
||||||
/* Zeilennummer am Knoten (D25): Grundlage für den Sprung ins Textfeld und
|
/* Zeilennummer am Knoten (D25): Grundlage für den Sprung ins Textfeld und
|
||||||
für die Gegenrichtung (Cursor-Zeile -> Knoten hervorheben). Der Hinweis im
|
für die Gegenrichtung (Cursor-Zeile -> Knoten hervorheben). Der Hinweis im
|
||||||
@@ -92,20 +97,62 @@ function nodeHtml(n, extra, opts){
|
|||||||
const riskMark = n.status && n.status.key === 'highrisk'
|
const riskMark = n.status && n.status.key === 'highrisk'
|
||||||
? `<span class="risk" aria-hidden="true" title="${attr(t('riskTooltip'))}">⚠︎</span>`
|
? `<span class="risk" aria-hidden="true" title="${attr(t('riskTooltip'))}">⚠︎</span>`
|
||||||
: '';
|
: '';
|
||||||
const inner = esc(n.label) +
|
/* Falt-Zeichen (SPEC §9/D38): ▾ offen, „▸ n" eingeklappt — das Klickziel
|
||||||
|
fürs Umklappen (der einfache Klick auf den Knoten bleibt der Link, §6).
|
||||||
|
aria-hidden: die Information steht im aria-label (a11yFolded). */
|
||||||
|
const foldHtml = fold
|
||||||
|
? `<span class="fold" aria-hidden="true">${fold.collapsed ? '▸ ' + fold.count : '▾'}</span>`
|
||||||
|
: '';
|
||||||
|
const expanded = fold ? ` aria-expanded="${!fold.collapsed}"` : '';
|
||||||
|
const inner = foldHtml +
|
||||||
|
esc(n.label) +
|
||||||
(n.url ? '<span class="ext" aria-hidden="true">↗</span>' : '') +
|
(n.url ? '<span class="ext" aria-hidden="true">↗</span>' : '') +
|
||||||
riskMark +
|
riskMark +
|
||||||
sizeBadge +
|
sizeBadge +
|
||||||
tagsHtml;
|
tagsHtml;
|
||||||
const aria = ` aria-label="${attr(nodeAria(n, opts))}"`;
|
const aria = ` aria-label="${attr(nodeAria(n, opts, fold))}"`;
|
||||||
const html = n.url
|
const html = n.url
|
||||||
? `<a class="${cls}" href="${attr(n.url)}" target="_blank" rel="noopener"${lineAttr}${aria}${title}>${inner}</a>`
|
? `<a class="${cls}" href="${attr(n.url)}" target="_blank" rel="noopener"${lineAttr}${aria}${expanded}${title}>${inner}</a>`
|
||||||
: `<div class="${cls}" tabindex="0"${lineAttr}${aria}${title}>${inner}</div>`;
|
: `<div class="${cls}" tabindex="0"${lineAttr}${aria}${expanded}${title}>${inner}</div>`;
|
||||||
const ghostTip = attr(t('ghostTooltip'));
|
const ghostTip = attr(t('ghostTooltip'));
|
||||||
const ghost = `<div class="ghost-node" aria-label="${ghostTip}" title="${ghostTip}">${esc(t('ghost'))}</div>`;
|
const ghost = `<div class="ghost-node" aria-label="${ghostTip}" title="${ghostTip}">${esc(t('ghost'))}</div>`;
|
||||||
return html + (need ? ghost : '');
|
return html + (need ? ghost : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Eingeklappter Teilbaum (SPEC §9/D38): Das HTML entfällt, aber die
|
||||||
|
Warnungen des verborgenen Teils werden trotzdem gemeldet — sie sind eine
|
||||||
|
Aussage über den TEXT, nicht über die Ansicht. Derselbe Lauf zählt die
|
||||||
|
verborgenen Knoten für das „▸ n"-Kennzeichen. */
|
||||||
|
function walkFolded(node, warnings, opts){
|
||||||
|
const kids = visibleChildren(node, opts.showDiscarded);
|
||||||
|
if(!kids.length) return 0;
|
||||||
|
const types = new Set(kids.map(k => k.type));
|
||||||
|
if(types.size > 1){
|
||||||
|
warnings.push({type: 'mixedGate', line: kids[0].line, label: node.label});
|
||||||
|
}
|
||||||
|
let count = kids.length;
|
||||||
|
for(const k of kids) count += walkFolded(k, warnings, opts);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ein Knoten samt <li> und (sofern nicht eingeklappt) seiner Kinder. */
|
||||||
|
function itemHtml(n, extra, warnings, opts){
|
||||||
|
const vk = visibleChildren(n, opts.showDiscarded);
|
||||||
|
const canFold = vk.length > 0;
|
||||||
|
const collapsed = canFold && !!(opts.collapsedSet && opts.collapsedSet.has(n));
|
||||||
|
const fold = canFold
|
||||||
|
? {collapsed, count: collapsed ? walkFolded(n, warnings, opts) : 0}
|
||||||
|
: null;
|
||||||
|
/* `opt` auch am <li>: den Abzweig zeichnen dessen Pseudoelemente, er wird
|
||||||
|
für optionale Knoten gestrichelt (D29). Eingeklappt ist der Knoten ein
|
||||||
|
Blatt — kein has-*-Layout, keine Kinderliste. */
|
||||||
|
const liCls = liClass(collapsed ? [] : vk, opts, n.optional);
|
||||||
|
return `<li${liCls}>` +
|
||||||
|
nodeHtml(n, extra, opts, fold) +
|
||||||
|
(collapsed ? '' : renderChildren(n, warnings, opts)) +
|
||||||
|
`</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderChildren(node, warnings, opts){
|
function renderChildren(node, warnings, opts){
|
||||||
const kids = visibleChildren(node, opts.showDiscarded);
|
const kids = visibleChildren(node, opts.showDiscarded);
|
||||||
if(!kids.length) return '';
|
if(!kids.length) return '';
|
||||||
@@ -122,16 +169,7 @@ function renderChildren(node, warnings, opts){
|
|||||||
die Klasse `or` (alle Modi, Export-Routing); `xor` ergänzt nur die
|
die Klasse `or` (alle Modi, Export-Routing); `xor` ergänzt nur die
|
||||||
„1"-Plakette an der Sammelleiste (D35). */
|
„1"-Plakette an der Sammelleiste (D35). */
|
||||||
const ulCls = gate === 'xor' ? 'or xor' : gate;
|
const ulCls = gate === 'xor' ? 'or xor' : gate;
|
||||||
const items = kids.map(k => {
|
const items = kids.map(k => itemHtml(k, extraCls(k, opts), warnings, opts)).join('');
|
||||||
const vk = visibleChildren(k, opts.showDiscarded);
|
|
||||||
/* `opt` auch am <li>: den Abzweig zeichnen dessen Pseudoelemente, er wird
|
|
||||||
für optionale Knoten gestrichelt (D29). */
|
|
||||||
const liCls = liClass(vk, opts, k.optional);
|
|
||||||
return `<li${liCls}>` +
|
|
||||||
nodeHtml(k, extraCls(k, opts), opts) +
|
|
||||||
renderChildren(k, warnings, opts) +
|
|
||||||
`</li>`;
|
|
||||||
}).join('');
|
|
||||||
return `<ul class="${ulCls}">${items}</ul>`;
|
return `<ul class="${ulCls}">${items}</ul>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,13 +178,8 @@ function renderChildren(node, warnings, opts){
|
|||||||
Leere Wurzelliste ⇒ leerer String. */
|
Leere Wurzelliste ⇒ leerer String. */
|
||||||
export function renderTreeHtml(roots, opts){
|
export function renderTreeHtml(roots, opts){
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
const html = roots.map(root => {
|
const html = roots.map(root =>
|
||||||
const vk = visibleChildren(root, opts.showDiscarded);
|
itemHtml(root, ('root-node ' + extraCls(root, opts)).trim(), warnings, opts)
|
||||||
const liCls = liClass(vk, opts, root.optional);
|
).join('');
|
||||||
return `<li${liCls}>` +
|
|
||||||
nodeHtml(root, ('root-node ' + extraCls(root, opts)).trim(), opts) +
|
|
||||||
renderChildren(root, warnings, opts) +
|
|
||||||
`</li>`;
|
|
||||||
}).join('');
|
|
||||||
return { html, warnings };
|
return { html, warnings };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -549,6 +549,18 @@
|
|||||||
}
|
}
|
||||||
a.node{text-decoration:none;cursor:pointer}
|
a.node{text-decoration:none;cursor:pointer}
|
||||||
a.node:hover{box-shadow:0 2px 8px rgba(36,52,71,.22)}
|
a.node:hover{box-shadow:0 2px 8px rgba(36,52,71,.22)}
|
||||||
|
/* Falt-Zeichen (SPEC §9, D38): ▾ offen / „▸ n" eingeklappt, Klickziel fürs
|
||||||
|
Umklappen. Gedeckt in Grau — es ist Bedienelement, keine Statusaussage;
|
||||||
|
beim Zeigen färbt es sich zur Tinte. */
|
||||||
|
.node .fold{
|
||||||
|
cursor:pointer;color:var(--muted);
|
||||||
|
font-size:.72em;margin-right:6px;
|
||||||
|
user-select:none;-webkit-user-select:none;
|
||||||
|
}
|
||||||
|
.node .fold:hover{color:var(--line)}
|
||||||
|
/* Der dunkle Wurzelknoten braucht ein helleres Zeichen als das Grau. */
|
||||||
|
.root-node .fold{color:rgba(255,255,255,.75)}
|
||||||
|
.root-node .fold:hover{color:#fff}
|
||||||
/* Sichtbarer Tastatur-Fokus (Knoten sind fokussierbar: Fokusreihenfolge =
|
/* Sichtbarer Tastatur-Fokus (Knoten sind fokussierbar: Fokusreihenfolge =
|
||||||
Lese-/Dokumentreihenfolge; Screenreader liest den aria-label). */
|
Lese-/Dokumentreihenfolge; Screenreader liest den aria-label). */
|
||||||
.node:focus-visible{outline:2px solid var(--or);outline-offset:2px;box-shadow:0 2px 8px rgba(36,52,71,.22)}
|
.node:focus-visible{outline:2px solid var(--or);outline-offset:2px;box-shadow:0 2px 8px rgba(36,52,71,.22)}
|
||||||
@@ -1040,5 +1052,8 @@
|
|||||||
/* Editierhilfe bzw. Zuruf, nicht drucken (D25, Fokusmarke SPEC §1) */
|
/* Editierhilfe bzw. Zuruf, nicht drucken (D25, Fokusmarke SPEC §1) */
|
||||||
.node.current,.node.focusmark{box-shadow:none!important}
|
.node.current,.node.focusmark{box-shadow:none!important}
|
||||||
.node.fresh{box-shadow:none!important} /* „neu seit dem letzten Besuch" ist persönlich (D28) */
|
.node.fresh{box-shadow:none!important} /* „neu seit dem letzten Besuch" ist persönlich (D28) */
|
||||||
|
/* Faltung (D38): das ▾ offener Knoten ist Bedienelement — nicht drucken;
|
||||||
|
das „▸ n" eingeklappter Knoten ist Kennzeichnung und bleibt. */
|
||||||
|
.node:not(.folded) .fold{display:none!important}
|
||||||
@page{margin:12mm}
|
@page{margin:12mm}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`renderTreeHtml — kanonisches Beispiel > Grundzustand (Pfad aus, verworfene aus): Struktur-Snapshot 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit · jumpHint">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig · jumpHint">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit · jumpHint">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant · jumpHint">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee · jumpHint">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
|
exports[`renderTreeHtml — kanonisches Beispiel > Grundzustand (Pfad aus, verworfene aus): Struktur-Snapshot 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" title="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" title="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" title="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
|
||||||
|
|
||||||
exports[`renderTreeHtml — kanonisches Beispiel > günstigster Pfad an: cheap/cheap-leaf + implizite M-Badges 1`] = `"<li class="has-and"><a class="node root-node cheap st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit · jumpHint">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node cheap st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig · jumpHint">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node cheap st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit · jumpHint">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node cheap cheap-leaf st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node cheap st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant · jumpHint">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus, a11ySizeImplicit" title="st_geplant · jumpHint">WordPress<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus, a11ySizeImplicit" title="st_idee · jumpHint">Headless CMS<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li><li class="has-or"><div class="node cheap st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee · jumpHint">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf" tabindex="0" data-line="15" aria-label="Cloud, a11ySizeImplicit" title="jumpHint">Cloud<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise, a11ySizeImplicit" title="jumpHint">On-Premise<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li>"`;
|
exports[`renderTreeHtml — kanonisches Beispiel > günstigster Pfad an: cheap/cheap-leaf + implizite M-Badges 1`] = `"<li class="has-and"><a class="node root-node cheap st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node cheap st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" title="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node cheap st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node cheap cheap-leaf st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node cheap st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" title="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus, a11ySizeImplicit" title="st_geplant · jumpHint">WordPress<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus, a11ySizeImplicit" title="st_idee · jumpHint">Headless CMS<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li><li class="has-or"><div class="node cheap st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" title="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf" tabindex="0" data-line="15" aria-label="Cloud, a11ySizeImplicit" title="jumpHint">Cloud<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise, a11ySizeImplicit" title="jumpHint">On-Premise<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li>"`;
|
||||||
|
|
||||||
exports[`renderTreeHtml — kanonisches Beispiel > verworfene einblenden: Eigenentwicklung erscheint (durchgestrichen) 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit · jumpHint">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig · jumpHint">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit · jumpHint">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant · jumpHint">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li><li><div class="node st-verworfen" tabindex="0" data-line="13" aria-label="Eigenentwicklung, a11yStatus" title="st_verworfen · jumpHint">Eigenentwicklung</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee · jumpHint">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
|
exports[`renderTreeHtml — kanonisches Beispiel > verworfene einblenden: Eigenentwicklung erscheint (durchgestrichen) 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" aria-expanded="true" title="st_fertig · jumpHint"><span class="fold" aria-hidden="true">▾</span>Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" aria-expanded="true" title="st_arbeit · jumpHint"><span class="fold" aria-hidden="true">▾</span>Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="opt"><div class="node opt st-idee" tabindex="0" data-line="9" aria-label="Dark Mode, a11yStatus, a11ySize, a11yOptional" title="st_idee · a11yOptional · jumpHint">Dark Mode<span class="size" aria-hidden="true">S</span></div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="10" aria-label="CMS-Anbindung, a11yStatus, a11ySize" aria-expanded="true" title="st_geplant · jumpHint"><span class="fold" aria-hidden="true">▾</span>CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="11" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="12" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li><li><div class="node st-verworfen" tabindex="0" data-line="13" aria-label="Eigenentwicklung, a11yStatus" title="st_verworfen · jumpHint">Eigenentwicklung</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="14" aria-label="Hosting, a11yStatus, a11ySize" aria-expanded="true" title="st_idee · jumpHint"><span class="fold" aria-hidden="true">▾</span>Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="15" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="16" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parse } from '../src/parser.js';
|
||||||
|
import { initialCollapsed, computeCheapSet } from '../src/model.js';
|
||||||
|
import { renderTreeHtml } from '../src/render.js';
|
||||||
|
|
||||||
|
const t = key => key;
|
||||||
|
const roots = txt => parse(txt).roots;
|
||||||
|
const render = (txt, collapsedSet) => renderTreeHtml(roots(txt),
|
||||||
|
{t, showDiscarded: false, cheapPath: false, cheapSet: new Set(),
|
||||||
|
collapsedSet: collapsedSet || new Set()});
|
||||||
|
/* Rendert mit dem Anfangszustand aus den Textmarken (wie app.js, ohne Overrides). */
|
||||||
|
const renderFolded = txt => {
|
||||||
|
const r = roots(txt);
|
||||||
|
return renderTreeHtml(r, {t, showDiscarded: false, cheapPath: false,
|
||||||
|
cheapSet: new Set(), collapsedSet: initialCollapsed(r, true)});
|
||||||
|
};
|
||||||
|
const collapsedLabels = (txt, rescueFocus = true) =>
|
||||||
|
[...initialCollapsed(roots(txt), rescueFocus)].map(n => n.label).sort();
|
||||||
|
|
||||||
|
/* Faltmarken `>`/`<` (SPEC §1/§9, D38): Anfangszustand der Faltung im Text. */
|
||||||
|
describe('Parser — Faltmarke zwischen Zeichen und Statusbox', () => {
|
||||||
|
it('erkennt `>` und `<` und nimmt sie aus dem Label', () => {
|
||||||
|
const [wurzel] = roots(`[ ] Wurzel\n - > [x] Zu (M)\n - < [ ] Auf`);
|
||||||
|
expect(wurzel.children.map(k => [k.label, k.fold, k.status?.key]))
|
||||||
|
.toEqual([['Zu', '>', 'fertig'], ['Auf', '<', 'geplant']]);
|
||||||
|
expect(wurzel.fold).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('erkennt die Marke am Wurzelknoten (ohne Zeichen) am Zeilenanfang', () => {
|
||||||
|
const [wurzel] = roots(`> [~] Kapitel\n - [ ] Kind`);
|
||||||
|
expect([wurzel.fold, wurzel.label]).toEqual(['>', 'Kapitel']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verlangt folgenden Leerraum — `- >Achtung` bleibt ein Label', () => {
|
||||||
|
const [wurzel] = roots(`[ ] Wurzel\n - >Achtung`);
|
||||||
|
expect(wurzel.children.map(k => [k.label, k.fold]))
|
||||||
|
.toEqual([['>Achtung', null]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt ein `>` mitten im Label unberührt', () => {
|
||||||
|
const [wurzel] = roots(`[ ] a > b`);
|
||||||
|
expect([wurzel.label, wurzel.fold]).toEqual(['a > b', null]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initialCollapsed — `>` klappt ein, `<` wandert die Faltung hinunter', () => {
|
||||||
|
it('klappt `>`-Knoten ein', () => {
|
||||||
|
expect(collapsedLabels(`[ ] W\n - > [ ] A\n - [ ] A1\n - [ ] B`))
|
||||||
|
.toEqual(['A']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('holt einen `<`-Teilbaum hervor: Vorfahr öffnet, Geschwister klappen ein', () => {
|
||||||
|
const txt = `> [ ] W
|
||||||
|
- [ ] A
|
||||||
|
- [ ] A1
|
||||||
|
- [ ] B
|
||||||
|
- < [ ] B1
|
||||||
|
- [ ] B1a
|
||||||
|
- [ ] C`;
|
||||||
|
/* W öffnet sich (Pfad zu B1), A klappt stattdessen ein; B öffnet den Weg,
|
||||||
|
B1 samt Teilbaum ist sichtbar. C ist Blatt und braucht keine Faltung. */
|
||||||
|
expect(collapsedLabels(txt)).toEqual(['A']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respektiert ein `>` innerhalb des hervorgeholten Teilbaums', () => {
|
||||||
|
const txt = `> [ ] W
|
||||||
|
- < [ ] B
|
||||||
|
- > [ ] B1
|
||||||
|
- [ ] B1a`;
|
||||||
|
expect(collapsedLabels(txt)).toEqual(['B1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('holt auch einen `!!!`-markierten Knoten hervor', () => {
|
||||||
|
const txt = `> [ ] W\n - [ ] A\n - [ ] A1\n - [ ] B !!!\n - [ ] B1`;
|
||||||
|
expect(collapsedLabels(txt)).toEqual(['A']);
|
||||||
|
/* … aber nur mit rescueFocus — headless-Aufrufer können es abschalten. */
|
||||||
|
expect(collapsedLabels(txt, false)).toEqual(['W']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt Bäume ohne Marken vollständig offen', () => {
|
||||||
|
expect(collapsedLabels(`[ ] W\n - [ ] A\n - [ ] B`)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Renderer — eingeklappte Teilbäume', () => {
|
||||||
|
const TXT = `[ ] W\n - > [~] A (M)\n - [ ] A1\n - [ ] A2\n - [ ] A2a\n - [ ] B`;
|
||||||
|
|
||||||
|
it('lässt die Kinder eines eingeklappten Knotens weg', () => {
|
||||||
|
const {html} = renderFolded(TXT);
|
||||||
|
expect(html).not.toContain('A1');
|
||||||
|
expect(html).toContain('>B<');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('kennzeichnet mit Klasse `folded` und „▸ n" (n = alle verborgenen Knoten)', () => {
|
||||||
|
const {html} = renderFolded(TXT);
|
||||||
|
expect(html).toContain('folded');
|
||||||
|
expect(html).toContain('<span class="fold" aria-hidden="true">▸ 3</span>');
|
||||||
|
expect(html).toContain('aria-expanded="false"');
|
||||||
|
expect(html).toContain('a11yFolded');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gibt offenen Eltern das ▾, Blättern gar kein Falt-Zeichen', () => {
|
||||||
|
const {html} = render(`[ ] W\n - [ ] Blatt`);
|
||||||
|
expect((html.match(/class="fold"/g) || []).length).toBe(1);
|
||||||
|
expect(html).toContain('>▾</span>');
|
||||||
|
expect(html).toContain('aria-expanded="true"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('macht das eingeklappte <li> zum Blatt (kein has-*-Layout)', () => {
|
||||||
|
/* Nur W verzweigt sichtbar; das eingeklappte A steht als klassenloses <li>. */
|
||||||
|
const {html} = renderFolded(TXT);
|
||||||
|
expect((html.match(/has-and|has-or/g) || []).length).toBe(1);
|
||||||
|
expect(html).toContain('<li><div class="node folded st-arbeit"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('meldet Warnungen aus dem verborgenen Teilbaum weiter', () => {
|
||||||
|
const txt = `[ ] W\n - > [ ] A\n | [ ] X\n - [ ] Y`;
|
||||||
|
const {html, warnings} = renderFolded(txt);
|
||||||
|
expect(html).not.toContain('>X<');
|
||||||
|
expect(warnings).toEqual([{type: 'mixedGate', line: 3, label: 'A'}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt den günstigsten Pfad unberührt — Faltung ist reine Ansicht', () => {
|
||||||
|
const r = roots(`[ ] W (XS)\n - > [ ] A (S)\n | [ ] A1 (XL)\n | [ ] A2 (S)`);
|
||||||
|
expect([...computeCheapSet(r)].map(n => n.label).sort())
|
||||||
|
.toEqual(['A', 'A2', 'W']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -110,7 +110,9 @@ describe('renderTreeHtml — Moduswechsel ist CSS, nicht Renderer', () => {
|
|||||||
(die als <a> gerendert werden) und Knoten in any-of-Gruppen. */
|
(die als <a> gerendert werden) und Knoten in any-of-Gruppen. */
|
||||||
describe('renderTreeHtml — data-line je Knoten (D25)', () => {
|
describe('renderTreeHtml — data-line je Knoten (D25)', () => {
|
||||||
const lineOf = (html, label) => {
|
const lineOf = (html, label) => {
|
||||||
const re = new RegExp('<(?:a|div) class="node[^"]*"[^>]*?data-line="(\\d+)"[^>]*>' + label);
|
/* Eltern-Knoten tragen seit D38 das Falt-Zeichen (▾/▸) vor dem Label. */
|
||||||
|
const re = new RegExp('<(?:a|div) class="node[^"]*"[^>]*?data-line="(\\d+)"[^>]*>'
|
||||||
|
+ '(?:<span class="fold"[^>]*>[^<]*</span>)?' + label);
|
||||||
const m = html.match(re);
|
const m = html.match(re);
|
||||||
return m ? Number(m[1]) : null;
|
return m ? Number(m[1]) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user