diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
index 7c8fb3d..9b92132 100644
--- a/docs/DECISIONS.md
+++ b/docs/DECISIONS.md
@@ -173,3 +173,23 @@ Größen-Badge (weiß mit Petrol-Rand statt gefüllt) samt Tooltip, damit die
Annahme sichtbar und von einer echten `(M)`-Angabe unterscheidbar bleibt. Die
Größen-Semantik in SPEC §5 bleibt unberührt (die Wertung gilt nur der
Pfad-Kostenrechnung).
+
+**Pfad-Linie + Stationspunkte (U-Bahn-Plan):** Zusätzlich zur Inversion fädelt
+eine gestrichelte, geschwungene Petrol-Linie durch die **Endknoten** des Pfads.
+Sie liegt **hinter** den Knoten (kräftig in den Lücken), mit einer **abgetönten
+Kopie davor** (deutet den Verlauf beim Durchschreiten eines Knotens nur an —
+Alternative zum harten „über allem"). Problem dabei: läuft die Linie durch
+Fremd-/Zwischenknoten, ist nicht unterscheidbar, ob diese dazugehören
+(besonders im kompakten Modus). Lösung: ein **blasser, großer Stationspunkt**
+nur an den echten Blättern — durchquerte Knoten bleiben punktlos. Blass+groß
+statt klein+satt, damit der Knotentext lesbar bleibt. Alles auch im
+Grafikexport (hinten/vorne-Schichtung nachgezeichnet).
+
+**Layout: all-of unter any-of (horizontal).** Zerlegt eine any-of-Alternative
+selbst wieder all-of (z. B. „Web+Nativ" → Web + Android + iOS), würde der
+breite horizontale Fächer den Alternativknoten zentrieren und samt Elternbaum
+weit nach rechts schieben. Deshalb wird dieser Teilbaum **nur horizontal**
+schmal **transponiert** gestapelt (Kinder untereinander, linker solider
+Verteiler — wie im kompakten Modus), passend zur gestapelten any-of-Spalte
+darüber. Vertikal/kompakt haben ihre eigene `ul.and`-Behandlung und bleiben
+unberührt.
diff --git a/docs/SPEC.md b/docs/SPEC.md
index 49dfd9f..8309dd6 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -69,10 +69,13 @@ Codiert als Checkbox nach dem Zeichen (Erweiterung der Markdown-Task-Syntax):
- Werte: `XS < S < M < L < XL < XXL`, notiert in Klammern, z. B. `(L)`.
- **Untergliederungsregel:** Ab `(M)` muss ein Element weiter zerlegt sein.
- Ein Element ≥ M **ohne Kinder** erhält einen Geister-Knoten
- „Untergliederung fehlt“ an gestrichelter Linie darunter (in `--warn`,
- `#B45309`). Der angedeutete Unterpunkt genügt als Hinweis; eine zusätzliche
- Umrandung des Knotens gibt es nicht.
+ Ein Element ≥ M **ohne Kinder** erhält einen Geister-Knoten an gestrichelter
+ Linie darunter (in `--warn`, `#B45309`). Sein Label ist knapp „…“; die
+ Erklärung („Ab Größe M sollte ein Element weiter untergliedert werden.“)
+ steht im Tooltip — der ausgeschriebene Text machte die Zelle sonst breiter
+ als der Knoten und verschob gestapelte Geschwister. Der angedeutete
+ Unterpunkt genügt als Hinweis; eine zusätzliche Umrandung des Knotens gibt es
+ nicht.
- Ausnahme: verworfene Elemente lösen die Regel nie aus.
- Anzeige: petrolfarbenes Badge (`--or`, `#0F766E`) mit weißer Schrift oben
rechts an der Knoten-Ecke.
@@ -174,7 +177,18 @@ any-of-Alternativen samt Teilbaum) treten zurück (blass, entsättigt); der
günstige Pfad hebt sich dadurch von selbst ab — kein zusätzlicher Rahmen an den
ohnehin dichten Knoten-Ecken. Wo die Größe **implizit** als `M` angenommen wird,
zeigt der Knoten ein **invertiertes** Größen-Badge (weiß mit petrolfarbenem
-Rand/Text statt gefüllt) mit erläuterndem Tooltip. Siehe D18.
+Rand/Text statt gefüllt) mit erläuterndem Tooltip.
+
+Zusätzlich fädelt eine **gestrichelte, geschwungene Petrol-Linie** durch die
+**Endknoten (Blätter)** des Pfads (Katmull-Rom-Spline in Dokument-Reihenfolge,
+in allen Modi). Die kräftige Linie liegt **hinter** den Knoten (nur in den
+Lücken voll sichtbar), eine **abgetönte Kopie** davor deutet den Verlauf beim
+Durchschreiten eines Knotens nur schwach an. An jedem echten Endknoten sitzt ein
+großer, **blasser Petrol-Stationspunkt** (U-Bahn-Plan-Prinzip: der Knotentext
+bleibt lesbar) — nur **durchquerte** Fremd- oder Zwischenknoten tragen keinen
+Punkt, sodass eindeutig bleibt, welche Knoten auf dem Pfad **enden**. Linie,
+abgetönte Kopie und Punkte erscheinen bei aktivem Umschalter auch im
+Grafikexport. Siehe D18.
### Grafikexport des Diagramms
Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen
diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md
index 4a4eaf5..7f57d88 100644
--- a/frontend/CLAUDE.md
+++ b/frontend/CLAUDE.md
@@ -22,6 +22,16 @@ Diagramm rechts, Toggles für transponierte Ansicht und verworfene Elemente).
`var(--or)` nur noch für UI-Akzente/Logo (SPEC §9, D15).
- Extraktionsreihenfolge im Parser nicht umstellen: Kommentar → Zeichen/
Status → URL → Größe → Tags (sonst kollidiert `@` in URLs).
+- Günstigster Pfad: `markCheapest()`/`cheapestCost()` markieren die nötigen
+ Knoten (Klassen `cheap`, `cheap-leaf`); `drawCheapPath()` zeichnet nach jedem
+ `render()` **und** nach `applyLayout()` zwei Overlay-SVGs in `#out` (hinten
+ kräftige Linie, vorne abgetönte Kopie + Stationspunkte). Overlays erben den
+ CSS-`zoom` von `#out`, Punkte in unskalierte `#out`-Koordinaten umrechnen
+ (`/zoom`). `diagramToSvg()` zeichnet dieselbe Linie/Punkte nach (SPEC §9, D18).
+- Zerlegt eine any-of-Alternative selbst all-of, wird der Teilbaum **nur
+ horizontal** schmal transponiert (`ul.or>li.has-and>ul.and`, siehe D18) —
+ sonst schiebt der breite Fächer den Elternbaum nach rechts. Bei Layout-
+ Umbauten dieses Nesting mitprüfen.
- „verworfen" ist per Default ausgeblendet; Filterlogik steckt in
`visibleChildren()` und muss bei Renderer-Umbauten erhalten bleiben.
- Zustand wird im `localStorage` gehalten (noch kein Backend): `werkbaum-lang`
diff --git a/frontend/index.html b/frontend/index.html
index 6e3cf77..718b44b 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -302,7 +302,7 @@
.panel.collapsed .zoomctl{display:none}
/* ---------- Baum ---------- */
- .tree{width:max-content;margin:0 auto;list-style:none}
+ .tree{width:max-content;margin:0 auto;list-style:none;position:relative}
.tree ul{list-style:none}
.tree li{display:flex;flex-direction:column;align-items:center;position:relative}
.tree > li + li{margin-top:36px}
@@ -360,6 +360,19 @@
.cheap-on .node:not(.cheap){opacity:.32;filter:saturate(.4)}
/* implizit als M angenommene Größe: invertiertes Badge (weiß statt petrol) */
.size.implicit{background:var(--card);color:var(--or);border-color:var(--or)}
+ /* Günstigster-Pfad-Linie: gestrichelt, geschwungen, petrol — fädelt durch
+ die Endknoten (Blätter) des Pfads. Zwei Overlays in #out (erben dessen
+ zoom): die kräftige Linie liegt HINTER den Knoten (nur in den Lücken
+ sichtbar), eine abgetönte Kopie liegt DAVOR (deutet den Verlauf beim
+ Durchschreiten von Knoten nur schwach an). Kleine Stationspunkte an den
+ Blättern zeigen, welche Knoten wirklich auf dem Pfad enden — durchquerte
+ Fremdknoten tragen keinen Punkt. Reihenfolge im DOM steuert vorn/hinten. */
+ .cheap-overlay{position:absolute;left:0;top:0;overflow:visible;pointer-events:none}
+ .cheap-path{fill:none;stroke:var(--or);stroke-width:2.5;stroke-linecap:round;
+ stroke-dasharray:8 6;opacity:.9}
+ .cheap-path.faint{opacity:.2}
+ /* Stationspunkt: groß, aber blass gefüllt — der Knotentext bleibt lesbar */
+ .cheap-dot{fill:var(--or);fill-opacity:.2;stroke:var(--or);stroke-opacity:.35;stroke-width:1.5}
/* --- ALL OF: Kinder nebeneinander, durchgezogener Verteiler --- */
ul.and{display:flex;align-items:flex-start;position:relative;padding-top:14px}
@@ -401,6 +414,27 @@
content:'';position:absolute;left:9px;top:0;height:100%;border-left:2px dashed var(--muted);
}
ul.or>li:last-child::after{height:23px}
+ /* all-of-Teilbaum UNTER einer any-of-Alternative (nur horizontal):
+ schmal transponiert stapeln — Kinder untereinander mit linkem, solidem
+ Verteiler (wie im kompakten Modus), statt breitem Fächer. Sonst zwingt der
+ breite Fächer ein Zentrieren, das den Alternativknoten (und damit den
+ ganzen Elternbaum) weit nach rechts schiebt. Passt zugleich stilistisch
+ zur gestapelten any-of-Spalte darüber. Vertikal/kompakt haben ihre eigene
+ `ul.and`-Behandlung und bleiben unberührt. */
+ .tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and{
+ flex-direction:column;align-items:flex-start;padding-top:14px;
+ }
+ .tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and::before{left:9px;height:14px}
+ .tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li{padding:5px 0 5px 30px}
+ .tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li::before{
+ left:9px;right:auto;top:23px;width:21px;height:0;
+ border:0;border-top:2px solid var(--line);border-radius:0;
+ }
+ .tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li::after{
+ left:9px;right:auto;top:0;width:0;height:100%;
+ border:0;border-left:2px solid var(--line);border-radius:0;
+ }
+ .tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li:last-child::after{height:23px}
ul.or .node{
border-color:var(--muted);
box-shadow:none;
@@ -669,7 +703,12 @@ const INITIAL = `%% Project structure – Sprint 14
- [x] Audience analysis (S)
- [x] Sitemap (XS)
- [~] Implementation (XL)
- - [/] Frontend (S) https://git.example.com/frontend @anna
+ - [~] Frontend (M) https://git.example.com/frontend @anna
+ | [ ] PWA (S)
+ | [ ] Web+Nativ
+ - [/] Web (S)
+ - [ ] Android (M)
+ - [ ] iOS (M)
- [ ] Backend (L) @ben @carla
- [ ] CMS integration (M)
| [ ] WordPress
@@ -781,7 +820,12 @@ function markCheapest(n){
for(const k of kids) markCheapest(k);
}
}
-function cheapCls(n){ return cheapPathOn && cheapSet.has(n) ? 'cheap' : ''; }
+function cheapCls(n){
+ if(!(cheapPathOn && cheapSet.has(n))) return '';
+ /* Endknoten des Pfads: kein Kind liegt (mehr) auf dem günstigen Pfad */
+ const leaf = !pathChildren(n).some(k => cheapSet.has(k));
+ return leaf ? 'cheap cheap-leaf' : 'cheap';
+}
function nodeHtml(n, extra){
const need = needsBreakdown(n);
@@ -853,6 +897,64 @@ function render(){
}).join('');
warnBox.innerHTML = warnings.map(w => `
⚠ ${w}
`).join('');
+ drawCheapPath();
+}
+
+/* ---------- Günstigster-Pfad-Linie ----------
+ Eine gestrichelte, geschwungene Petrol-Linie fädelt durch die Endknoten
+ (Blätter) des günstigen Pfads. Das Overlay-SVG liegt in #out und erbt damit
+ dessen CSS-`zoom`; die Punkte werden in unskalierte #out-Koordinaten
+ umgerechnet (getBoundingClientRect / zoom). Neu gezeichnet nach jedem
+ render() und nach Moduswechsel (applyLayout ruft nicht render). */
+const SVGNS = 'http://www.w3.org/2000/svg';
+function catmullRom(p){
+ if(p.length < 2) return '';
+ if(p.length === 2) return `M${p[0].x.toFixed(1)},${p[0].y.toFixed(1)} L${p[1].x.toFixed(1)},${p[1].y.toFixed(1)}`;
+ let d = `M${p[0].x.toFixed(1)},${p[0].y.toFixed(1)}`;
+ for(let i = 0; i < p.length - 1; i++){
+ const p0 = p[i-1] || p[i], p1 = p[i], p2 = p[i+1], p3 = p[i+2] || p2;
+ const c1x = p1.x + (p2.x - p0.x)/6, c1y = p1.y + (p2.y - p0.y)/6;
+ const c2x = p2.x - (p3.x - p1.x)/6, c2y = p2.y - (p3.y - p1.y)/6;
+ d += ` C${c1x.toFixed(1)},${c1y.toFixed(1)} ${c2x.toFixed(1)},${c2y.toFixed(1)} ${p2.x.toFixed(1)},${p2.y.toFixed(1)}`;
+ }
+ return d;
+}
+function svgEl(name, attrs){
+ const e = document.createElementNS(SVGNS, name);
+ for(const k in attrs) e.setAttribute(k, attrs[k]);
+ return e;
+}
+function overlaySvg(cls, w, h){
+ const svg = svgEl('svg', {class:'cheap-overlay ' + cls, width:w, height:h,
+ viewBox:`0 0 ${w.toFixed(1)} ${h.toFixed(1)}`});
+ return svg;
+}
+function drawCheapPath(){
+ out.querySelectorAll('svg.cheap-overlay').forEach(e => e.remove());
+ if(!cheapPathOn) return;
+ const leaves = [...out.querySelectorAll('.node.cheap-leaf')]; /* Dokument-Reihenfolge = Lese-Reihenfolge */
+ if(leaves.length < 2) return;
+ const outRect = out.getBoundingClientRect();
+ const z = zoom || 1;
+ if(!outRect.width || !outRect.height) return; /* Panel eingeklappt */
+ const pts = leaves.map(el => {
+ const r = el.getBoundingClientRect();
+ return {x:(r.left + r.width/2 - outRect.left)/z, y:(r.top + r.height/2 - outRect.top)/z};
+ });
+ const w = outRect.width/z, h = outRect.height/z;
+ const d = catmullRom(pts);
+
+ /* kräftige Linie HINTER die Knoten (als erstes Kind → hinterste Paint-Ebene) */
+ const back = overlaySvg('cheap-back', w, h);
+ back.appendChild(svgEl('path', {class:'cheap-path', d}));
+ out.insertBefore(back, out.firstChild);
+
+ /* davor: abgetönte Kopie (deutet den Verlauf über Knoten an) + Stationspunkte */
+ const front = overlaySvg('cheap-front', w, h);
+ front.appendChild(svgEl('path', {class:'cheap-path faint', d}));
+ pts.forEach(p => front.appendChild(
+ svgEl('circle', {class:'cheap-dot', cx:p.x.toFixed(1), cy:p.y.toFixed(1), r:10})));
+ out.appendChild(front);
}
/* ---------- Diagramm als Grafik (SVG → PNG) ---------- */
@@ -873,6 +975,11 @@ function diagramToSvg(){
cx:r.left+ox+r.width/2, cy:r.top+oy+r.height/2, r:r.right+ox, b:r.bottom+oy}; };
const parts = [``];
const nodes = [...out.querySelectorAll('.node')];
+ const cheapPts = cheapPathOn
+ ? [...out.querySelectorAll('.node.cheap-leaf')].map(el => { const b = R(el); return {x:b.cx, y:b.cy}; })
+ : [];
+ const cheapLine = op =>
+ ``;
/* 1) Verbindungslinien (hinter den Knoten) */
const seg = (x1,y1,x2,y2,stroke,dash) =>
@@ -909,6 +1016,9 @@ function diagramToSvg(){
}
});
+ /* 1b) Günstigster-Pfad: kräftige Linie hinter den Knoten */
+ if(cheapPts.length >= 2) parts.push(cheapLine('0.9'));
+
/* 2) Badge/Pille (Größe, Tags) */
const drawBadge = (el, fill, textColor, strokeColor) => {
const b = R(el);
@@ -934,6 +1044,13 @@ function diagramToSvg(){
});
});
+ /* 3b) Günstigster-Pfad: abgetönte Kopie über den Knoten + Stationspunkte */
+ if(cheapPts.length >= 2){
+ parts.push(cheapLine('0.2'));
+ cheapPts.forEach(p => parts.push(
+ ``));
+ }
+
/* 4) Geister-Knoten „Untergliederung fehlt“ */
out.querySelectorAll('.ghost-node').forEach(g => {
const b = R(g);
@@ -1020,7 +1137,8 @@ function applyLayout(mode){
out.classList.toggle('vertical', mode === 'vertikal');
out.classList.toggle('kompakt', mode === 'kompakt');
app.classList.toggle('side', mode !== 'horizontal');
- applySplit(); /* Preset für die neue Orientierung neu setzen */
+ applySplit(); /* Preset für die neue Orientierung neu setzen */
+ drawCheapPath(); /* Blatt-Positionen ändern sich mit dem Modus */
}
document.querySelectorAll('input[name="layout"]').forEach(radio => {
radio.addEventListener('change', () => { if(radio.checked) applyLayout(radio.value); });
@@ -1224,7 +1342,7 @@ const I18N = {
modeVertikal:"Vertikal – Baum nach rechts, Diagramm neben dem Editor",
zoomOut:"verkleinern", zoomReset:"zurücksetzen", zoomIn:"vergrößern",
zoomAria:"Zoom (Strg/Cmd + Mausrad)", langMore:"weitere Sprachen",
- empty:"Noch keine Struktur — einfach lostippen.", ghost:"Untergliederung fehlt",
+ empty:"Noch keine Struktur — einfach lostippen.", ghost:"…",
mixedWarn:"Zeile {line}: Unter „{label}“ sind - und | gemischt — dargestellt nach dem ersten Kind.",
st_idee:"Idee", st_geplant:"geplant", st_arbeit:"in Arbeit", st_durchstich:"Durchstich",
st_fertig:"fertig", st_prod:"in Produktion", st_verworfen:"verworfen",
@@ -1260,7 +1378,7 @@ const I18N = {
modeVertikal:"Vertical – tree to the right, diagram beside the editor",
zoomOut:"zoom out", zoomReset:"reset", zoomIn:"zoom in",
zoomAria:"Zoom (Ctrl/Cmd + mouse wheel)", langMore:"more languages",
- empty:"No structure yet — just start typing.", ghost:"Breakdown missing",
+ empty:"No structure yet — just start typing.", ghost:"…",
mixedWarn:"Line {line}: under “{label}”, - and | are mixed — rendered by the first child.",
st_idee:"idea", st_geplant:"planned", st_arbeit:"in progress", st_durchstich:"walking skeleton",
st_fertig:"done", st_prod:"in production", st_verworfen:"discarded",
@@ -1296,7 +1414,7 @@ const I18N = {
modeVertikal:"Vertical – árbol hacia la derecha, diagrama junto al editor",
zoomOut:"alejar", zoomReset:"restablecer", zoomIn:"acercar",
zoomAria:"Zoom (Ctrl/Cmd + rueda del ratón)", langMore:"más idiomas",
- empty:"Aún no hay estructura — simplemente empieza a escribir.", ghost:"Falta el desglose",
+ empty:"Aún no hay estructura — simplemente empieza a escribir.", ghost:"…",
mixedWarn:"Línea {line}: bajo «{label}» se mezclan - y | — se representa según el primer hijo.",
st_idee:"idea", st_geplant:"planificado", st_arbeit:"en curso", st_durchstich:"prototipo funcional",
st_fertig:"terminado", st_prod:"en producción", st_verworfen:"descartado",
@@ -1332,7 +1450,7 @@ const I18N = {
modeVertikal:"Vertical – arbre vers la droite, diagramme à côté de l'éditeur",
zoomOut:"dézoomer", zoomReset:"réinitialiser", zoomIn:"zoomer",
zoomAria:"Zoom (Ctrl/Cmd + molette)", langMore:"plus de langues",
- empty:"Pas encore de structure — commencez à taper.", ghost:"Décomposition manquante",
+ empty:"Pas encore de structure — commencez à taper.", ghost:"…",
mixedWarn:"Ligne {line} : sous « {label} », - et | sont mélangés — rendu selon le premier enfant.",
st_idee:"idée", st_geplant:"planifié", st_arbeit:"en cours", st_durchstich:"squelette fonctionnel",
st_fertig:"terminé", st_prod:"en production", st_verworfen:"abandonné",
@@ -1368,7 +1486,7 @@ const I18N = {
modeVertikal:"Pionowo – drzewo w prawo, diagram obok edytora",
zoomOut:"pomniejsz", zoomReset:"resetuj", zoomIn:"powiększ",
zoomAria:"Powiększenie (Ctrl/Cmd + kółko myszy)", langMore:"więcej języków",
- empty:"Brak struktury — zacznij pisać.", ghost:"Brak podziału",
+ empty:"Brak struktury — zacznij pisać.", ghost:"…",
mixedWarn:"Wiersz {line}: pod „{label}” mieszają się - i | — renderowane według pierwszego dziecka.",
st_idee:"pomysł", st_geplant:"zaplanowane", st_arbeit:"w toku", st_durchstich:"działający szkielet",
st_fertig:"gotowe", st_prod:"w produkcji", st_verworfen:"odrzucone",
@@ -1404,7 +1522,7 @@ const I18N = {
modeVertikal:"Вертикально – дерево вправо, диаграмма рядом с редактором",
zoomOut:"уменьшить", zoomReset:"сбросить", zoomIn:"увеличить",
zoomAria:"Масштаб (Ctrl/Cmd + колесо мыши)", langMore:"ещё языки",
- empty:"Пока нет структуры — просто начните печатать.", ghost:"Нет декомпозиции",
+ empty:"Пока нет структуры — просто начните печатать.", ghost:"…",
mixedWarn:"Строка {line}: под «{label}» смешаны - и | — отображается по первому потомку.",
st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
st_fertig:"готово", st_prod:"в эксплуатации", st_verworfen:"отклонено",
@@ -1440,7 +1558,7 @@ const I18N = {
modeVertikal:"लंबवत – पेड़ दाईं ओर, संपादक के बगल में आरेख",
zoomOut:"ज़ूम आउट", zoomReset:"रीसेट करें", zoomIn:"ज़ूम इन",
zoomAria:"ज़ूम (Ctrl/Cmd + माउस-व्हील)", langMore:"और भाषाएँ",
- empty:"अभी कोई संरचना नहीं — बस टाइप करना शुरू करें।", ghost:"विभाजन अनुपस्थित",
+ empty:"अभी कोई संरचना नहीं — बस टाइप करना शुरू करें।", ghost:"…",
mixedWarn:"पंक्ति {line}: „{label}“ के अंतर्गत - और | मिश्रित हैं — पहले चाइल्ड के अनुसार दिखाया गया।",
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_verworfen:"अस्वीकृत",
@@ -1476,7 +1594,7 @@ const I18N = {
modeVertikal:"纵向——树向右展开,图表在编辑器旁边",
zoomOut:"缩小", zoomReset:"重置", zoomIn:"放大",
zoomAria:"缩放(Ctrl/Cmd + 鼠标滚轮)", langMore:"更多语言",
- empty:"还没有结构——直接开始输入吧。", ghost:"缺少细分",
+ empty:"还没有结构——直接开始输入吧。", ghost:"…",
mixedWarn:"第 {line} 行:在「{label}」下 - 与 | 混用——按第一个子项渲染。",
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
st_fertig:"已完成", st_prod:"已上线", st_verworfen:"已放弃",
@@ -1512,7 +1630,7 @@ const I18N = {
modeVertikal:"縦 — ツリーを右へ、ダイアグラムはエディターの横",
zoomOut:"縮小", zoomReset:"リセット", zoomIn:"拡大",
zoomAria:"ズーム(Ctrl/Cmd + マウスホイール)", langMore:"その他の言語",
- empty:"まだ構造がありません — 入力を始めてください。", ghost:"分解がありません",
+ empty:"まだ構造がありません — 入力を始めてください。", ghost:"…",
mixedWarn:"{line} 行目:「{label}」の下で - と | が混在 — 最初の子に従って表示。",
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
st_fertig:"完了", st_prod:"本番稼働", st_verworfen:"破棄",