Erklärt in allen sieben Sprachen die Regel aus SPEC §5: ab T-Shirt-Größe M sollte ein Element weiter untergliedert werden. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1222 lines
65 KiB
HTML
1222 lines
65 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Werkbaum – WBS-Editor für Projektstrukturplan & Feature-Tree (Und/Oder-Baum)</title>
|
||
<meta name="description" content="Werkbaum ist ein textbasierter Editor für Projektstrukturpläne (PSP / Work Breakdown Structure, WBS): Struktur als Text schreiben, das Und/Oder-Baum-Diagramm entsteht live. Ebenso als Feature-Tree für Anforderungen (Requirements) und Aufwandsschätzung.">
|
||
<link rel="icon" type="image/svg+xml" href="../docs/brand/favicon.svg">
|
||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--paper:#F3F6F9;
|
||
--grid:rgba(36,52,71,.055);
|
||
--ink:#243447;
|
||
--line:#41556E;
|
||
--card:#FFFFFF;
|
||
--muted:#6B7A8C;
|
||
--or:#0F766E;
|
||
--warn:#B45309;
|
||
--r:8px;
|
||
}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{
|
||
font-family:'IBM Plex Sans',system-ui,sans-serif;
|
||
color:var(--ink);
|
||
background:
|
||
repeating-linear-gradient(0deg,transparent 0 23px,var(--grid) 23px 24px),
|
||
repeating-linear-gradient(90deg,transparent 0 23px,var(--grid) 23px 24px),
|
||
var(--paper);
|
||
height:100vh;overflow:hidden;
|
||
display:flex;flex-direction:column;
|
||
padding:24px 20px 12px;
|
||
}
|
||
header{max-width:1200px;width:100%;margin:0 auto 16px;flex:0 0 auto}
|
||
.header-row{display:flex;align-items:center;justify-content:space-between;gap:16px}
|
||
.header-tools{display:flex;align-items:center;gap:12px}
|
||
.fsbtn{
|
||
display:inline-flex;align-items:center;justify-content:center;
|
||
width:30px;height:29px;padding:0;cursor:pointer;
|
||
border:1px solid rgba(36,52,71,.2);border-radius:8px;
|
||
background:var(--card);color:var(--muted);
|
||
}
|
||
.fsbtn svg{display:block}
|
||
.fsbtn:hover{border-color:var(--or);color:var(--or)}
|
||
.fsbtn.active{background:var(--or);color:#fff;border-color:var(--or)}
|
||
/* Vollbild: Panels nutzen die ganze Fensterbreite (kein linker/rechter Rand). */
|
||
body.fullscreen{padding-left:0;padding-right:0}
|
||
body.fullscreen header,
|
||
body.fullscreen .app,
|
||
body.fullscreen .site-footer{max-width:none}
|
||
h1{font-size:1.25rem;font-weight:600;letter-spacing:-.01em}
|
||
header p{margin-top:4px;color:var(--muted);font-size:.9rem}
|
||
.langsel{
|
||
display:inline-flex;flex:0 0 auto;
|
||
border:1px solid rgba(36,52,71,.2);border-radius:999px;overflow:hidden;
|
||
}
|
||
.langsel button{
|
||
border:0;background:var(--card);cursor:pointer;color:var(--muted);
|
||
font-family:'IBM Plex Mono',monospace;font-size:.72rem;letter-spacing:.04em;
|
||
padding:5px 10px;
|
||
}
|
||
.langsel button + button{border-left:1px solid rgba(36,52,71,.14)}
|
||
.langsel button:hover:not(.active){color:var(--or)}
|
||
.langsel button.active{background:var(--or);color:#fff}
|
||
.langmore{letter-spacing:0;padding:5px 8px;font-weight:600}
|
||
.langsel .lang-extra{display:none} /* PL/RU/HI erst nach Aufklappen */
|
||
.langsel.expanded .lang-extra{display:block}
|
||
.site-footer{
|
||
max-width:1200px;margin:28px auto 0;
|
||
color:var(--muted);font-size:.78rem;text-align:center;
|
||
}
|
||
.site-footer a{color:var(--or);text-decoration:none}
|
||
.site-footer a:hover{text-decoration:underline}
|
||
|
||
/* Horizontal (Toggle aus): Diagramm oben, Editor unten — je volle Breite.
|
||
Vertikal (Toggle an, .side): Editor links, Diagramm rechts.
|
||
Zwischen beiden liegt ein verschiebbarer Gutter (Splitter). */
|
||
.app{
|
||
max-width:1200px;width:100%;margin:0 auto;
|
||
flex:1 1 auto;min-height:0;
|
||
display:grid;gap:0;
|
||
grid-template-columns:minmax(0,1fr);
|
||
grid-template-rows:var(--drow,60%) 14px minmax(0,1fr);
|
||
grid-template-areas:"diagram" "gutter" "editor";
|
||
align-items:stretch;
|
||
}
|
||
.app.side{
|
||
grid-template-columns:var(--col,360px) 14px minmax(0,1fr);
|
||
grid-template-rows:minmax(0,1fr);
|
||
grid-template-areas:"editor gutter diagram";
|
||
}
|
||
.panel.editor{grid-area:editor}
|
||
.panel.right{grid-area:diagram}
|
||
.gutter{grid-area:gutter}
|
||
/* Splitter/Gutter: stapelweise horizontal, seitlich vertikal. */
|
||
.gutter{
|
||
position:relative;align-self:stretch;justify-self:stretch;
|
||
height:14px;cursor:row-resize;touch-action:none;
|
||
}
|
||
.app.side .gutter{height:auto;width:14px;cursor:col-resize}
|
||
.gutter::before{
|
||
content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
|
||
width:46px;height:4px;border-radius:999px;background:rgba(36,52,71,.16);
|
||
transition:background .15s;
|
||
}
|
||
.app.side .gutter::before{width:4px;height:46px}
|
||
.gutter:hover::before,.gutter.dragging::before{background:var(--or)}
|
||
|
||
/* Minimieren: Panel auf seine Titelzeile schrumpfen, Körper ausblenden.
|
||
'a' = Diagramm minimiert (Editor maximal), 'b' = Editor minimiert. */
|
||
.panel.collapsed .editor-body,
|
||
.panel.collapsed .diagram,
|
||
.panel.collapsed .warnings{display:none}
|
||
.panel.collapsed .copybtn,
|
||
.panel.collapsed .switch,
|
||
.panel.collapsed .seg{display:none}
|
||
.panel.collapsed .panel-head{cursor:pointer}
|
||
.panel.collapsed .panel-head>span{cursor:pointer}
|
||
/* Gestapelt: kollabiertes Panel = Titelzeile (auto), anderes füllt. */
|
||
.app:not(.side).collapse-diagram{grid-template-rows:auto 14px minmax(0,1fr)}
|
||
.app:not(.side).collapse-editor{grid-template-rows:minmax(0,1fr) 14px auto}
|
||
/* Seitlich: schmaler vertikaler Streifen mit gedrehter Beschriftung. */
|
||
.app.side.collapse-editor{grid-template-columns:auto 14px minmax(0,1fr)}
|
||
.app.side.collapse-diagram{grid-template-columns:minmax(0,1fr) 14px auto}
|
||
.app.side .panel.collapsed{align-self:stretch}
|
||
.app.side .panel.collapsed .panel-head{
|
||
flex-direction:column;align-items:center;gap:12px;
|
||
height:100%;padding:12px 6px;border-bottom:0;
|
||
}
|
||
.app.side .panel.collapsed .panel-head>span{
|
||
writing-mode:vertical-rl;transform:rotate(180deg);
|
||
margin:0;white-space:nowrap;
|
||
}
|
||
.app.side .panel.collapsed .winbtns{flex-direction:column}
|
||
|
||
/* Schmaler Viewport: Seitenmodus fällt auf gestapelt zurück. */
|
||
@media (max-width:820px){
|
||
.app.side{
|
||
grid-template-columns:minmax(0,1fr);
|
||
grid-template-rows:var(--drow,60%) 14px minmax(0,1fr);
|
||
grid-template-areas:"diagram" "gutter" "editor";
|
||
}
|
||
.app.side.collapse-diagram{grid-template-columns:minmax(0,1fr);grid-template-rows:auto 14px minmax(0,1fr)}
|
||
.app.side.collapse-editor{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr) 14px auto}
|
||
.app.side .panel.collapsed .panel-head{flex-direction:row;align-items:center;height:auto;padding:10px 16px}
|
||
.app.side .panel.collapsed .panel-head>span{writing-mode:horizontal-tb;transform:none}
|
||
.app.side .panel.collapsed .winbtns{flex-direction:row}
|
||
.app.side .gutter{height:14px;width:auto;cursor:row-resize}
|
||
.app.side .gutter::before{width:46px;height:4px}
|
||
}
|
||
|
||
/* Editor-Innenaufbau: Textarea + aufklappbare Agenda.
|
||
Horizontal nebeneinander, vertikal (.side) untereinander. */
|
||
.editor-body{display:flex;align-items:stretch;flex:1 1 auto;min-height:0}
|
||
.editor-body textarea{flex:1 1 auto;min-width:0;min-height:0}
|
||
.agenda{
|
||
flex:0 0 auto;
|
||
border-left:1px dashed rgba(36,52,71,.18);
|
||
display:flex;flex-direction:column;
|
||
}
|
||
.agenda[open]{width:300px}
|
||
.agenda>summary{
|
||
list-style:none;cursor:pointer;white-space:nowrap;
|
||
padding:10px 16px;
|
||
font-family:'IBM Plex Mono',monospace;
|
||
font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);
|
||
display:flex;align-items:center;gap:7px;
|
||
}
|
||
.agenda>summary::-webkit-details-marker{display:none}
|
||
.agenda>summary::before{content:'▸';display:inline-block;transition:transform .15s}
|
||
.agenda[open]>summary::before{transform:rotate(90deg)}
|
||
.agenda{min-height:0}
|
||
.agenda[open]{overflow:hidden}
|
||
.agenda .hint{border-top:0;flex:1 1 auto;min-height:0;overflow:auto}
|
||
.side .editor-body{flex-direction:column;align-items:stretch}
|
||
.side .agenda{border-left:0;border-top:1px dashed rgba(36,52,71,.18)}
|
||
.side .agenda[open]{width:auto}
|
||
|
||
.panel{
|
||
background:var(--card);
|
||
border:1px solid rgba(15,118,110,.30); /* dezentes Petrol */
|
||
border-radius:12px;
|
||
overflow:hidden;
|
||
display:flex;flex-direction:column;min-height:0;min-width:0;
|
||
}
|
||
.panel-head{
|
||
flex:0 0 auto;
|
||
padding:10px 16px;
|
||
display:flex;justify-content:space-between;align-items:center;gap:14px;
|
||
font-family:'IBM Plex Mono',monospace;
|
||
font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;
|
||
color:var(--muted);
|
||
border-bottom:1px solid rgba(36,52,71,.1);
|
||
flex-wrap:wrap;
|
||
}
|
||
.panel-head>span{margin-right:auto}
|
||
.switch{
|
||
display:flex;align-items:center;gap:7px;cursor:pointer;
|
||
text-transform:none;letter-spacing:0;
|
||
font-family:'IBM Plex Sans',sans-serif;font-size:.78rem;color:var(--muted);
|
||
}
|
||
.switch input{position:absolute;opacity:0;width:0;height:0}
|
||
.switch i{
|
||
width:30px;height:17px;border-radius:999px;background:#C6CFD8;
|
||
position:relative;transition:background .15s;flex:0 0 auto;
|
||
}
|
||
.switch i::before{
|
||
content:'';position:absolute;top:2px;left:2px;width:13px;height:13px;
|
||
border-radius:50%;background:#fff;transition:left .15s;
|
||
box-shadow:0 1px 2px rgba(36,52,71,.25);
|
||
}
|
||
.switch input:checked + i{background:var(--or)}
|
||
.switch input:checked + i::before{left:15px}
|
||
.seg{
|
||
display:inline-flex;align-items:stretch;
|
||
border:1px solid rgba(36,52,71,.2);border-radius:999px;overflow:hidden;
|
||
}
|
||
.seg label{display:flex;cursor:pointer}
|
||
.seg input{position:absolute;opacity:0;width:0;height:0}
|
||
.seg span{
|
||
display:grid;place-items:center;padding:4px 10px;color:var(--muted);
|
||
transition:background .15s,color .15s;
|
||
}
|
||
.seg span svg{display:block}
|
||
.seg label + label span{border-left:1px solid rgba(36,52,71,.14)}
|
||
.seg input:checked + span{background:var(--or);color:#fff}
|
||
.seg input:focus-visible + span{outline:2px solid var(--or);outline-offset:-2px}
|
||
.winbtns{display:inline-flex;gap:4px;align-items:center}
|
||
.winbtn{
|
||
width:19px;height:19px;display:grid;place-items:center;
|
||
border:1px solid rgba(36,52,71,.2);border-radius:5px;
|
||
background:var(--card);color:var(--muted);cursor:pointer;
|
||
font-size:.72rem;line-height:1;padding:0;
|
||
}
|
||
.winbtn:hover{border-color:var(--or);color:var(--or)}
|
||
.winbtn.active{background:var(--or);color:#fff;border-color:var(--or)}
|
||
.copybtn{
|
||
color:var(--muted);background:var(--card);
|
||
border:1px solid rgba(36,52,71,.2);border-radius:6px;
|
||
padding:3px 6px;cursor:pointer;
|
||
display:inline-flex;align-items:center;justify-content:center;
|
||
}
|
||
.copybtn:hover,.copybtn.done{border-color:var(--or);color:var(--or)}
|
||
.copybtn svg{display:block;width:15px;height:15px}
|
||
.copybtn .ic-done{display:none}
|
||
.copybtn.done .ic-copy{display:none}
|
||
.copybtn.done .ic-done{display:block}
|
||
textarea{
|
||
display:block;width:100%;min-height:0;
|
||
border:0;outline:none;resize:none;
|
||
padding:14px 16px;
|
||
font-family:'IBM Plex Mono',monospace;
|
||
font-size:.85rem;line-height:1.7;
|
||
color:var(--ink);background:var(--card);
|
||
tab-size:2;
|
||
}
|
||
.hint{
|
||
padding:12px 16px;border-top:1px dashed rgba(36,52,71,.18);
|
||
font-size:.8rem;color:var(--muted);line-height:1.7;
|
||
}
|
||
.hint code{
|
||
font-family:'IBM Plex Mono',monospace;font-size:.75rem;
|
||
background:var(--paper);border:1px solid rgba(36,52,71,.12);
|
||
border-radius:4px;padding:0 5px;
|
||
}
|
||
.hint .or-code{color:var(--or);border-color:rgba(15,118,110,.4)}
|
||
|
||
.diagram{
|
||
flex:1 1 auto;min-height:0;
|
||
padding:28px 16px 24px;
|
||
overflow:auto;
|
||
background:transparent;
|
||
}
|
||
.panel.right{background:rgba(255,255,255,.6);position:relative}
|
||
.warnings{padding:0 16px 14px;font-size:.8rem;color:var(--warn)}
|
||
.warnings div{margin-top:4px}
|
||
.empty{color:var(--muted);font-size:.85rem;padding:8px 4px}
|
||
|
||
/* Zoom-Steuerung: schwebt unten rechts über dem Diagramm. */
|
||
.zoomctl{
|
||
position:absolute;right:14px;bottom:14px;z-index:5;
|
||
display:inline-flex;align-items:center;gap:2px;padding:2px;
|
||
background:var(--card);border:1px solid rgba(36,52,71,.18);
|
||
border-radius:8px;box-shadow:0 1px 4px rgba(36,52,71,.14);
|
||
}
|
||
.zoomctl button{
|
||
border:0;background:transparent;cursor:pointer;color:var(--muted);
|
||
font-family:'IBM Plex Mono',monospace;font-size:.82rem;line-height:1;
|
||
padding:4px 8px;border-radius:6px;min-width:30px;
|
||
}
|
||
#zoomReset{min-width:48px;font-size:.72rem}
|
||
.zoomctl button:hover{background:var(--paper);color:var(--or)}
|
||
.panel.collapsed .zoomctl{display:none}
|
||
|
||
/* ---------- Baum ---------- */
|
||
.tree{width:max-content;margin:0 auto;list-style:none}
|
||
.tree ul{list-style:none}
|
||
.tree li{display:flex;flex-direction:column;align-items:center;position:relative}
|
||
.tree > li + li{margin-top:36px}
|
||
|
||
.node{
|
||
position:relative;
|
||
color:var(--ink);
|
||
background:var(--card);
|
||
border:1.5px solid var(--line);
|
||
border-radius:var(--r);
|
||
padding:7px 14px;
|
||
font-size:.88rem;font-weight:500;line-height:1.3;
|
||
white-space:nowrap;
|
||
box-shadow:0 1px 2px rgba(36,52,71,.08);
|
||
}
|
||
a.node{text-decoration:none;cursor:pointer}
|
||
a.node:hover{box-shadow:0 2px 8px rgba(36,52,71,.22)}
|
||
.ext{font-size:.72em;margin-left:5px;opacity:.55}
|
||
.size{
|
||
position:absolute;top:-9px;right:-9px;
|
||
font-family:'IBM Plex Mono',monospace;
|
||
font-size:.58rem;font-weight:500;letter-spacing:.02em;
|
||
background:var(--or);color:#fff;
|
||
border:1.5px solid var(--card);
|
||
border-radius:999px;padding:1px 6px;line-height:1.4;
|
||
}
|
||
.tags{
|
||
position:absolute;right:-9px;bottom:-9px;
|
||
display:flex;gap:3px;
|
||
}
|
||
.tag{
|
||
font-family:'IBM Plex Mono',monospace;
|
||
font-size:.58rem;font-weight:500;letter-spacing:.02em;
|
||
background:#EEF1F4;color:var(--ink);
|
||
border:1.5px solid var(--line);
|
||
border-radius:999px;padding:1px 6px;line-height:1.4;
|
||
box-shadow:0 0 0 1.5px var(--card);
|
||
}
|
||
.ghost-node{
|
||
position:relative;margin-top:14px;
|
||
border:1.5px dashed var(--warn);
|
||
color:var(--warn);background:rgba(180,83,9,.06);
|
||
border-radius:var(--r);padding:4px 12px;
|
||
font-size:.74rem;font-style:italic;white-space:nowrap;
|
||
}
|
||
.ghost-node::before{
|
||
content:'';position:absolute;top:-14px;left:50%;height:14px;
|
||
border-left:2px dashed var(--warn);
|
||
}
|
||
.root-node{background:var(--ink);border-color:var(--ink);color:#fff;font-weight:600;padding:9px 20px}
|
||
|
||
/* --- ALL OF: Kinder nebeneinander, durchgezogener Verteiler --- */
|
||
ul.and{display:flex;align-items:flex-start;position:relative;padding-top:14px}
|
||
ul.and::before{
|
||
content:'';position:absolute;top:0;left:50%;height:14px;border-left:2px solid var(--line);
|
||
}
|
||
ul.and>li{padding:16px 12px 0}
|
||
ul.and>li::before,
|
||
ul.and>li::after{
|
||
content:'';position:absolute;top:0;right:50%;
|
||
border-top:2px solid var(--line);width:50%;height:16px;
|
||
}
|
||
ul.and>li::after{right:auto;left:50%;border-left:2px solid var(--line)}
|
||
ul.and>li:only-child{padding-top:14px}
|
||
ul.and>li:only-child::before,ul.and>li:only-child::after{border-top:none}
|
||
ul.and>li:only-child::after{border-left:2px solid var(--line);height:14px}
|
||
ul.and>li:first-child::before,
|
||
ul.and>li:last-child::after{border:0 none}
|
||
ul.and>li:last-child::before{border-right:2px solid var(--line);border-radius:0 6px 0 0}
|
||
ul.and>li:first-child::after{border-radius:6px 0 0 0}
|
||
|
||
/* --- ANY OF: durchgehend gestrichelte graue Linien (Haupt- + Abzweige) --- */
|
||
li.has-or{align-items:flex-start}
|
||
ul.or{
|
||
display:flex;flex-direction:column;
|
||
align-items:flex-start;
|
||
position:relative;
|
||
padding-top:14px;
|
||
}
|
||
ul.or::before{
|
||
content:'';position:absolute;top:0;left:9px;height:14px;border-left:2px dashed var(--muted);
|
||
}
|
||
ul.or>li{position:relative;align-items:flex-start;padding:5px 0 5px 30px}
|
||
/* Abzweig auf Höhe der Knoten-Mitte (5px li-Padding + halbe Knotenhöhe), nicht Mitte des Teilbaums */
|
||
ul.or>li::before{
|
||
content:'';position:absolute;left:9px;top:23px;width:21px;border-top:2px dashed var(--muted);
|
||
}
|
||
ul.or>li::after{
|
||
content:'';position:absolute;left:9px;top:0;height:100%;border-left:2px dashed var(--muted);
|
||
}
|
||
ul.or>li:last-child::after{height:23px}
|
||
ul.or .node{
|
||
border-color:var(--muted);
|
||
box-shadow:none;
|
||
font-weight:400;
|
||
}
|
||
|
||
/* ---------- Status (Pastell) ---------- */
|
||
.node.st-idee {background:#EBEDEF;border-color:#A2ABB5;color:var(--ink)}
|
||
.node.st-geplant {background:#EBE4F6;border-color:#A991D4;color:var(--ink)}
|
||
.node.st-arbeit {background:#FADDE4;border-color:#D897A8;color:var(--ink)}
|
||
.node.st-durchstich{background:#FBF2CE;border-color:#D9BE63;color:var(--ink)}
|
||
.node.st-fertig {background:#DCF1DE;border-color:#86C293;color:var(--ink)}
|
||
.node.st-prod {background:#DBEAF8;border-color:#85ACD7;color:var(--ink)}
|
||
.node.st-verworfen{
|
||
background:#F1F2F4;border-color:#B3BAC2;border-style:dashed;
|
||
color:var(--muted);text-decoration:line-through;box-shadow:none;
|
||
}
|
||
|
||
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
||
.chip{
|
||
font-family:'IBM Plex Mono',monospace;font-size:.72rem;
|
||
border:1.5px solid;border-radius:6px;padding:2px 8px;
|
||
color:var(--ink);white-space:nowrap;
|
||
}
|
||
.chip.st-idee {background:#EBEDEF;border-color:#A2ABB5}
|
||
.chip.st-geplant {background:#EBE4F6;border-color:#A991D4}
|
||
.chip.st-arbeit {background:#FADDE4;border-color:#D897A8}
|
||
.chip.st-durchstich{background:#FBF2CE;border-color:#D9BE63}
|
||
.chip.st-fertig {background:#DCF1DE;border-color:#86C293}
|
||
.chip.st-prod {background:#DBEAF8;border-color:#85ACD7}
|
||
.chip.st-verworfen{background:#F1F2F4;border-color:#B3BAC2;border-style:dashed;color:var(--muted);text-decoration:line-through}
|
||
|
||
/* ---------- Transponierte Modi (vertikal + kompakt) ---------- */
|
||
/* Gemeinsame Basis: Kinder untereinander, Sammelleiste links,
|
||
Abzweig auf Knotenmitte (23-px-Offset). */
|
||
.tree.vertical, .tree.kompakt{margin:0}
|
||
.tree.vertical li, .tree.kompakt li{align-items:flex-start}
|
||
.tree.vertical ul.and, .tree.kompakt ul.and{flex-direction:column;align-items:flex-start;padding-top:14px}
|
||
.tree.vertical ul.and::before, .tree.kompakt ul.and::before{left:9px;height:14px}
|
||
.tree.vertical ul.and>li, .tree.kompakt ul.and>li{padding:5px 0 5px 30px}
|
||
.tree.vertical ul.and>li::before, .tree.kompakt 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.vertical ul.and>li::after, .tree.kompakt 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.vertical ul.and>li:last-child::after, .tree.kompakt ul.and>li:last-child::after{height:23px}
|
||
|
||
/* Nur vertikal: echter transponierter, ZENTRIERTER LR-Baum — analog zur
|
||
horizontalen Darstellung, nur um 90° gedreht. Der Überknoten sitzt
|
||
vertikal mittig zu seiner Kindergruppe; die all-of-Verbindung tritt rechts
|
||
auf halber Höhe aus, eine vertikale Sammelleiste verteilt auf die Kinder.
|
||
Kompakt bleibt unberührt (führt all-of nach unten). */
|
||
.tree.vertical li.has-and{flex-direction:row;align-items:center}
|
||
.tree.vertical li.has-and>ul.and{padding-top:0}
|
||
/* Stub von der Parent-Mitte (halbe Höhe) zur vertikalen Sammelleiste */
|
||
.tree.vertical li.has-and>ul.and::before{
|
||
left:0;top:50%;width:9px;height:0;
|
||
border-left:0;border-top:2px solid var(--line);
|
||
}
|
||
/* Abzweig trifft die tatsächliche Knotenmitte des Kindes:
|
||
Blatt- und Any-of-Kinder sitzen oben (Offset 23 px), all-of-Zwischenknoten
|
||
sind selbst vertikal zentriert (50 %). Ohne diese Unterscheidung liefen die
|
||
Linien zu zentrierten Zwischenknoten ins Leere. Die Enden der Sammelleiste
|
||
folgen demselben Abzweigpunkt des jeweiligen Rand-Kindes. */
|
||
.tree.vertical li.has-and>ul.and>li:first-child::after{top:23px;height:calc(100% - 23px)}
|
||
.tree.vertical li.has-and>ul.and>li:only-child::after{border:0}
|
||
.tree.vertical li.has-and>ul.and>li.has-and::before{top:50%}
|
||
.tree.vertical li.has-and>ul.and>li.has-and:first-child::after{top:50%;height:50%}
|
||
.tree.vertical li.has-and>ul.and>li.has-and:last-child::after{top:0;height:50%}
|
||
.tree.vertical li.has-and>ul.and>li.has-and:only-child::after{border:0}
|
||
|
||
/* Kompakt: auch All-of läuft nach unten (spart Breite) — kein Rechts-Fächer.
|
||
Die graue, gestrichelte Any-of-Führung kommt aus der Basis und gilt hier
|
||
wie in allen Modi. Gate-Codierung: und = durchgezogen dunkel (Tinte),
|
||
oder = gestrichelt grau. */
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div class="header-row">
|
||
<h1 style="display:flex;align-items:center;gap:11px">
|
||
<svg width="34" height="34" viewBox="0 0 64 64" aria-hidden="true">
|
||
<rect x="22" y="5" width="20" height="12" rx="3.5" fill="#243447"/>
|
||
<path d="M32 17 V22 M14 22 H50 M14 22 V27 M50 22 V27" fill="none" stroke="#243447" stroke-width="3"/>
|
||
<rect x="4" y="27" width="20" height="12" rx="3.5" fill="#fff" stroke="#243447" stroke-width="3"/>
|
||
<rect x="40" y="27" width="20" height="12" rx="3.5" fill="#fff" stroke="#243447" stroke-width="3"/>
|
||
<path d="M50 39 V46" fill="none" stroke="#0F766E" stroke-width="3" stroke-dasharray="4 4"/>
|
||
<rect x="40" y="46" width="20" height="12" rx="3.5" fill="#fff" stroke="#0F766E" stroke-width="3"/>
|
||
</svg>
|
||
<span data-i18n-title="brandTooltip" title="„Werkbaum“ bedeutet so viel wie ‚Werk-Baum‘ — der Baum des Projektstrukturplans (WBS).">Werk<span style="color:var(--or)">baum</span></span>
|
||
</h1>
|
||
<div class="header-tools">
|
||
<button type="button" id="fsToggle" class="fsbtn" aria-pressed="false" data-i18n-title="fullscreenTooltip" data-i18n-aria="fullscreenTooltip" title="Vollbild – Panels nutzen die ganze Fensterbreite" aria-label="Vollbild – Panels nutzen die ganze Fensterbreite">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
|
||
</button>
|
||
<div class="langsel" role="radiogroup" aria-label="Sprache / Language">
|
||
<button type="button" data-lang="de" title="Deutsch">DE</button>
|
||
<button type="button" data-lang="en" title="English">EN</button>
|
||
<button type="button" data-lang="es" title="Español">ES</button>
|
||
<button type="button" data-lang="fr" title="Français">FR</button>
|
||
<button type="button" class="lang-extra" data-lang="pl" title="Polski">PL</button>
|
||
<button type="button" class="lang-extra" data-lang="ru" title="Русский">RU</button>
|
||
<button type="button" class="lang-extra" data-lang="hi" title="हिन्दी">HI</button>
|
||
<button type="button" id="langMore" class="langmore" aria-expanded="false" data-i18n-title="langMore" data-i18n-aria="langMore" title="weitere Sprachen" aria-label="weitere Sprachen">…</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p data-i18n="subtitle">Werkbaum – Ein Text-basierter WBS/Projektstrukturplan/Feature-Tree Diagramm-Editor.</p>
|
||
</header>
|
||
|
||
<div class="app" id="app">
|
||
<div class="panel editor">
|
||
<div class="panel-head">
|
||
<span data-i18n="editorTitle">Struktur (Text)</span>
|
||
<button type="button" class="copybtn" id="copy" data-i18n-title="copyTooltip" data-i18n-aria="copyTooltip" title="Text in die Zwischenablage kopieren" aria-label="Text in die Zwischenablage kopieren">
|
||
<svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
|
||
</button>
|
||
<div class="winbtns" aria-label="Editor">
|
||
<button type="button" class="winbtn" data-state="b" data-i18n-title="minimize" title="minimieren">—</button>
|
||
<button type="button" class="winbtn" data-state="normal" data-i18n-title="normal" title="normal">▢</button>
|
||
<button type="button" class="winbtn" data-state="a" data-i18n-title="maximize" title="maximieren">■</button>
|
||
</div>
|
||
</div>
|
||
<div class="editor-body">
|
||
<textarea id="src" spellcheck="false"></textarea>
|
||
<details class="agenda">
|
||
<summary data-i18n="agenda">Agenda</summary>
|
||
<div class="hint" id="hint">
|
||
Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.<br>
|
||
<code>-</code> Teilpaket, alle erforderlich <em>(all of)</em><br>
|
||
<code class="or-code">|</code> Alternative, eine wählen <em>(any of)</em><br>
|
||
Zeile ohne Zeichen = Wurzelknoten. Geschwister sollten dasselbe Zeichen tragen.<br>
|
||
Status als Kästchen nach dem Zeichen, z. B. <code>- [~] Frontend</code>:
|
||
<div class="chips">
|
||
<span class="chip st-idee">[?] Idee</span>
|
||
<span class="chip st-geplant">[ ] geplant</span>
|
||
<span class="chip st-arbeit">[~] in Arbeit</span>
|
||
<span class="chip st-durchstich">[/] Durchstich</span>
|
||
<span class="chip st-fertig">[x] fertig</span>
|
||
<span class="chip st-prod">[^] in Produktion</span>
|
||
<span class="chip st-verworfen">[-] verworfen</span>
|
||
</div>
|
||
Aufwand als T-Shirt-Größe in Klammern, Link einfach als URL anhängen:<br>
|
||
<code>- [ ] Backend (L) https://…</code><br>
|
||
Ab <code>(M)</code> gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.<br>
|
||
Kommentare mit <code>%%</code> — als ganze Zeile oder am Zeilenende.
|
||
Personen mit <code>@name</code> — erscheinen unten rechts am Knoten.
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="gutter" id="gutter" role="separator" aria-label="Bereiche größenverändern" title="Ziehen zum Verschieben"></div>
|
||
|
||
<div class="panel right">
|
||
<div class="panel-head">
|
||
<span data-i18n="diagramTitle">Diagramm</span>
|
||
<label class="switch" data-i18n-title="discardedTooltip" title="Verworfene Knoten samt Teilbaum ein-/ausblenden">
|
||
<input type="checkbox" id="showc"><i></i><span data-i18n="discarded">verworfene</span>
|
||
</label>
|
||
<div class="seg" role="radiogroup" aria-label="Darstellung">
|
||
<label data-i18n-title="modeHorizontal" title="Horizontal – Organigramm, Diagramm über dem Editor">
|
||
<input type="radio" name="layout" value="horizontal" checked data-i18n-aria="modeHorizontal" aria-label="Horizontal – Organigramm">
|
||
<span><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="3" width="6" height="4" rx="1"/><rect x="2.5" y="17" width="6" height="4" rx="1"/><rect x="15.5" y="17" width="6" height="4" rx="1"/><path d="M12 7V13M5.5 13H18.5M5.5 13V17M18.5 13V17"/></svg></span>
|
||
</label>
|
||
<label data-i18n-title="modeKompakt" title="Kompakt – alles nach unten, platzsparend">
|
||
<input type="radio" name="layout" value="kompakt" data-i18n-aria="modeKompakt" aria-label="Kompakt – alles nach unten">
|
||
<span><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="6" height="4" rx="1"/><path d="M6 7V20"/><path d="M6 11H10M6 17H10"/><rect x="10" y="9" width="9" height="4" rx="1"/><rect x="10" y="15" width="9" height="4" rx="1"/></svg></span>
|
||
</label>
|
||
<label data-i18n-title="modeVertikal" title="Vertikal – Baum nach rechts, Diagramm neben dem Editor">
|
||
<input type="radio" name="layout" value="vertikal" data-i18n-aria="modeVertikal" aria-label="Vertikal – Baum nach rechts">
|
||
<span><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="9.5" width="5" height="5" rx="1"/><rect x="16" y="3" width="6" height="4" rx="1"/><rect x="16" y="17" width="6" height="4" rx="1"/><path d="M7.5 12H12M12 5V19M12 5H16M12 19H16"/></svg></span>
|
||
</label>
|
||
</div>
|
||
<button type="button" class="copybtn" id="copyDiagram" data-i18n-title="copyDiagramTooltip" data-i18n-aria="copyDiagramTooltip" title="Diagramm als Textbaum kopieren" aria-label="Diagramm als Textbaum kopieren">
|
||
<svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
|
||
</button>
|
||
<div class="winbtns" aria-label="Diagramm">
|
||
<button type="button" class="winbtn" data-state="a" data-i18n-title="minimize" title="minimieren">—</button>
|
||
<button type="button" class="winbtn" data-state="normal" data-i18n-title="normal" title="normal">▢</button>
|
||
<button type="button" class="winbtn" data-state="b" data-i18n-title="maximize" title="maximieren">■</button>
|
||
</div>
|
||
</div>
|
||
<div class="diagram"><ul class="tree" id="out"></ul></div>
|
||
<div class="warnings" id="warn"></div>
|
||
<div class="zoomctl" data-i18n-aria="zoomAria" aria-label="Zoom (Strg/Cmd + Mausrad)">
|
||
<button type="button" id="zoomOut" data-i18n-title="zoomOut" title="verkleinern">−</button>
|
||
<button type="button" id="zoomReset" data-i18n-title="zoomReset" title="zurücksetzen">100 %</button>
|
||
<button type="button" id="zoomIn" data-i18n-title="zoomIn" title="vergrößern">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<footer class="site-footer"><a href="https://github.com/mhoennig/werkbaum" target="_blank" rel="noopener">Werkbaum 1.0</a> – © 2026 <a href="https://michael.hoennig.de" target="_blank" rel="noopener">Michael Hönnig</a> · Vibe-Coded with Claude Code Fable 5 and Opus 4.8 – <a href="../LICENSE" target="_blank" rel="noopener">MIT-License</a></footer>
|
||
|
||
<script>
|
||
const INITIAL = `%% Projektstruktur – Stand Sprint 14
|
||
[~] Website-Relaunch (XL) https://wiki.example.de/relaunch
|
||
- [x] Konzeption (M)
|
||
- [x] Zielgruppenanalyse (S)
|
||
- [x] Sitemap (XS)
|
||
- [~] Umsetzung (XL)
|
||
- [/] Frontend (S) https://git.example.de/frontend @anna
|
||
- [ ] Backend (L) @ben @carla
|
||
- [ ] CMS-Anbindung (M)
|
||
| [ ] WordPress
|
||
| [?] Headless CMS
|
||
| [-] Eigenentwicklung %% Aufwand zu hoch
|
||
- [?] Hosting (M)
|
||
| Cloud
|
||
| On-Premise`;
|
||
|
||
const SIZE_RANK = {XS:0, S:1, M:2, L:3, XL:4, XXL:5};
|
||
|
||
const STATUS_BY_CODE = {
|
||
'?': {key:'idee', name:'Idee'},
|
||
' ': {key:'geplant', name:'geplant'},
|
||
'~': {key:'arbeit', name:'in Arbeit'},
|
||
'/': {key:'durchstich', name:'Durchstich – funktionsbereit, Feinarbeiten offen'},
|
||
'x': {key:'fertig', name:'fertig'},
|
||
'^': {key:'prod', name:'in Produktion'},
|
||
'-': {key:'verworfen', name:'verworfen'}
|
||
};
|
||
|
||
/* Rück-Abbildung Status-Key → Code (für den Text-Export des Diagramms) */
|
||
const CODE_BY_KEY = {idee:'?', geplant:' ', arbeit:'~', durchstich:'/', fertig:'x', prod:'^', verworfen:'-'};
|
||
|
||
const src = document.getElementById('src');
|
||
const out = document.getElementById('out');
|
||
const warnBox = document.getElementById('warn');
|
||
|
||
function esc(s){
|
||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
/* ---------- Parser ---------- */
|
||
function parse(text){
|
||
const virtualRoot = {label:'', type:'and', children:[]};
|
||
const stack = [{node:virtualRoot, width:-1}];
|
||
const warnings = [];
|
||
|
||
text.split('\n').forEach((raw, i) => {
|
||
raw = raw.replace(/%%.*$/, ''); /* %%-Kommentare entfernen (Mermaid-Konvention) */
|
||
if(!raw.trim()) return;
|
||
const m = raw.match(/^([ \t]*)([-|])?\s*(?:\[([ ?~xX^\/-])]\s*)?(.*)$/);
|
||
const width = m[1].replace(/\t/g,' ').length;
|
||
const type = m[2] === '|' ? 'or' : 'and';
|
||
const status = m[3] ? STATUS_BY_CODE[m[3].toLowerCase()] : null;
|
||
|
||
let rest = m[4], url = null, size = null;
|
||
const tags = [];
|
||
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(/@([\p{L}\p{N}._-]+)/gu, (s, g) => { tags.push(g); return ''; });
|
||
const label = rest.replace(/\s+/g, ' ').trim();
|
||
if(!label) return;
|
||
|
||
while(stack.length > 1 && stack[stack.length-1].width >= width) stack.pop();
|
||
const parent = stack[stack.length-1].node;
|
||
|
||
const node = {label, type, status, url, size, tags, children:[], line:i+1};
|
||
parent.children.push(node);
|
||
stack.push({node, width});
|
||
});
|
||
|
||
return {roots: virtualRoot.children, warnings};
|
||
}
|
||
|
||
/* ---------- Renderer ---------- */
|
||
function gateOf(children){
|
||
return children.length && children[0].type === 'or' ? 'or' : 'and';
|
||
}
|
||
|
||
function needsBreakdown(n){
|
||
if(n.status && n.status.key === 'verworfen') return false;
|
||
return !!n.size && SIZE_RANK[n.size] >= SIZE_RANK.M && !n.children.length;
|
||
}
|
||
|
||
function visibleChildren(n){
|
||
if(showc.checked) return n.children;
|
||
return n.children.filter(k => !k.status || k.status.key !== 'verworfen');
|
||
}
|
||
|
||
function nodeHtml(n, extra){
|
||
const need = needsBreakdown(n);
|
||
const cls = ['node', extra || '', n.status ? 'st-' + n.status.key : '']
|
||
.filter(Boolean).join(' ');
|
||
const title = n.status ? ` title="${esc(t('st_' + n.status.key)).replace(/"/g,'"')}"` : '';
|
||
const tagsHtml = n.tags && n.tags.length
|
||
? `<span class="tags">${n.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}</span>`
|
||
: '';
|
||
const inner = esc(n.label) +
|
||
(n.url ? '<span class="ext">↗</span>' : '') +
|
||
(n.size ? `<span class="size">${n.size}</span>` : '') +
|
||
tagsHtml;
|
||
const html = n.url
|
||
? `<a class="${cls}" href="${esc(n.url).replace(/"/g,'"')}" target="_blank" rel="noopener"${title}>${inner}</a>`
|
||
: `<div class="${cls}"${title}>${inner}</div>`;
|
||
const ghostTip = esc(t('ghostTooltip')).replace(/"/g,'"');
|
||
const ghost = `<div class="ghost-node" title="${ghostTip}">${esc(t('ghost'))}</div>`;
|
||
return html + (need ? ghost : '');
|
||
}
|
||
|
||
function renderChildren(node, warnings){
|
||
const kids = visibleChildren(node);
|
||
if(!kids.length) return '';
|
||
const types = new Set(kids.map(k => k.type));
|
||
if(types.size > 1){
|
||
warnings.push(t('mixedWarn', {line: kids[0].line, label: esc(node.label)}));
|
||
}
|
||
const gate = gateOf(kids);
|
||
const items = kids.map(k => {
|
||
const vk = visibleChildren(k);
|
||
const liCls = vk.length ? (gateOf(vk) === 'or' ? ' class="has-or"' : ' class="has-and"') : '';
|
||
return `<li${liCls}>` +
|
||
nodeHtml(k) +
|
||
renderChildren(k, warnings) +
|
||
`</li>`;
|
||
}).join('');
|
||
return `<ul class="${gate}">${items}</ul>`;
|
||
}
|
||
|
||
function render(){
|
||
let {roots} = parse(src.value);
|
||
const warnings = [];
|
||
if(!showc.checked){
|
||
roots = roots.filter(r => !r.status || r.status.key !== 'verworfen');
|
||
}
|
||
|
||
if(!roots.length){
|
||
out.innerHTML = `<div class="empty">${esc(t('empty'))}</div>`;
|
||
warnBox.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
out.innerHTML = roots.map(root => {
|
||
const vk = visibleChildren(root);
|
||
const liCls = vk.length ? (gateOf(vk) === 'or' ? ' class="has-or"' : ' class="has-and"') : '';
|
||
return `<li${liCls}>` +
|
||
nodeHtml(root, 'root-node') +
|
||
renderChildren(root, warnings) +
|
||
`</li>`;
|
||
}).join('');
|
||
|
||
warnBox.innerHTML = warnings.map(w => `<div>⚠ ${w}</div>`).join('');
|
||
}
|
||
|
||
/* ---------- Diagramm als einfacher Text-Baum ---------- */
|
||
/* Einfachstes, aber überall einsetzbares Format: eingerückter ASCII-Baum.
|
||
all-of-Abzweige mit ├─/└─, any-of-Alternativen mit ├◇/└◇. Je Knoten:
|
||
[Status] Label (Größe) @tags. Respektiert den „verworfene“-Filter, gibt
|
||
also genau die sichtbare Struktur wieder. */
|
||
function nodeLabelText(n){
|
||
let s = n.status ? '[' + (CODE_BY_KEY[n.status.key] ?? ' ') + '] ' : '';
|
||
s += n.label;
|
||
if(n.size) s += ' (' + n.size + ')';
|
||
if(n.tags && n.tags.length) s += ' ' + n.tags.map(x => '@' + x).join(' ');
|
||
return s;
|
||
}
|
||
function walkText(node, prefix, lines){
|
||
const kids = visibleChildren(node);
|
||
const gate = gateOf(kids);
|
||
kids.forEach((k, i) => {
|
||
const last = i === kids.length - 1;
|
||
const conn = gate === 'or'
|
||
? (last ? '└◇ ' : '├◇ ')
|
||
: (last ? '└─ ' : '├─ ');
|
||
lines.push(prefix + conn + nodeLabelText(k));
|
||
walkText(k, prefix + (last ? ' ' : '│ '), lines);
|
||
});
|
||
}
|
||
function treeToText(){
|
||
let {roots} = parse(src.value);
|
||
if(!showc.checked){
|
||
roots = roots.filter(r => !r.status || r.status.key !== 'verworfen');
|
||
}
|
||
const blocks = roots.map(root => {
|
||
const lines = [nodeLabelText(root)];
|
||
walkText(root, '', lines);
|
||
return lines.join('\n');
|
||
});
|
||
return blocks.join('\n\n'); /* mehrere Wurzeln durch Leerzeile trennen */
|
||
}
|
||
|
||
/* Tab-Taste rückt ein statt den Fokus zu wechseln */
|
||
src.addEventListener('keydown', e => {
|
||
if(e.key === 'Tab'){
|
||
e.preventDefault();
|
||
const {selectionStart:s, selectionEnd:eEnd, value} = src;
|
||
src.value = value.slice(0, s) + ' ' + value.slice(eEnd);
|
||
src.selectionStart = src.selectionEnd = s + 2;
|
||
render();
|
||
}
|
||
});
|
||
|
||
src.addEventListener('input', render);
|
||
|
||
const app = document.getElementById('app');
|
||
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 */
|
||
}
|
||
document.querySelectorAll('input[name="layout"]').forEach(radio => {
|
||
radio.addEventListener('change', () => { if(radio.checked) applyLayout(radio.value); });
|
||
});
|
||
|
||
/* ---------- Min/Normal/Max je Bereich (Fenster-Buttons) ---------- */
|
||
/* Zustand 'a' = Diagramm minimiert (Editor maximiert),
|
||
'b' = Editor minimiert (Diagramm maximiert), 'normal' = beide sichtbar.
|
||
Minimieren schrumpft ein Panel auf seine Titelzeile; Maximieren des einen
|
||
entspricht dem Minimieren des anderen. */
|
||
let splitState = 'normal';
|
||
const editorPanel = document.querySelector('.panel.editor');
|
||
const diagramPanel = document.querySelector('.panel.right');
|
||
function clearCollapse(){
|
||
app.classList.remove('collapse-editor','collapse-diagram');
|
||
editorPanel.classList.remove('collapsed');
|
||
diagramPanel.classList.remove('collapsed');
|
||
document.querySelectorAll('.winbtn').forEach(b => b.classList.remove('active'));
|
||
}
|
||
function applySplit(){
|
||
/* Preset-/Drag-Größen bei Preset-Wechsel zurücksetzen */
|
||
app.style.removeProperty('--col');
|
||
app.style.removeProperty('--drow');
|
||
app.classList.toggle('collapse-diagram', splitState==='a');
|
||
app.classList.toggle('collapse-editor', splitState==='b');
|
||
diagramPanel.classList.toggle('collapsed', splitState==='a');
|
||
editorPanel.classList.toggle('collapsed', splitState==='b');
|
||
document.querySelectorAll('.winbtn').forEach(b => b.classList.toggle('active', b.dataset.state===splitState));
|
||
}
|
||
/* Setzt einen Minimier-Zustand während des Ziehens nur bei Änderung. */
|
||
function snapTo(state){ if(splitState!==state){ splitState = state; applySplit(); } }
|
||
document.querySelectorAll('.winbtn').forEach(b => {
|
||
b.addEventListener('click', e => {
|
||
/* Fenster-Buttons sind maßgeblich: nicht zum Titelzeilen-Restore durchreichen */
|
||
e.stopPropagation();
|
||
splitState = b.dataset.state; applySplit();
|
||
});
|
||
});
|
||
/* Klick auf die Titelzeile eines minimierten Panels stellt es wieder her. */
|
||
[editorPanel, diagramPanel].forEach(panel => {
|
||
panel.querySelector('.panel-head').addEventListener('click', e => {
|
||
if(e.target.closest('button, label, input')) return; /* Bedienelemente behalten ihre Funktion */
|
||
if(panel.classList.contains('collapsed')){ splitState = 'normal'; applySplit(); }
|
||
});
|
||
});
|
||
|
||
/* ---------- Splitter: Bereiche per Drag verteilen ---------- */
|
||
const gutter = document.getElementById('gutter');
|
||
const diagramEl = document.querySelector('.diagram');
|
||
let dragging = false;
|
||
gutter.addEventListener('pointerdown', e => {
|
||
dragging = true;
|
||
gutter.classList.add('dragging');
|
||
gutter.setPointerCapture(e.pointerId);
|
||
document.body.style.userSelect = 'none';
|
||
/* Freies Ziehen hebt Preset und Minimierung auf */
|
||
splitState = 'custom';
|
||
clearCollapse();
|
||
e.preventDefault();
|
||
});
|
||
/* Näher als SNAP an einem Rand: in den minimierten Zustand einrasten,
|
||
sonst frei skalieren. So sind beide Extreme per Splitter erreichbar. */
|
||
const SNAP = 72;
|
||
gutter.addEventListener('pointermove', e => {
|
||
if(!dragging) return;
|
||
const rect = app.getBoundingClientRect();
|
||
if(app.classList.contains('side')){
|
||
const w = e.clientX - rect.left, maxw = rect.width - 14;
|
||
if(w <= SNAP) snapTo('b'); /* Editor auf Titelzeile */
|
||
else if(w >= maxw - SNAP) snapTo('a'); /* Diagramm auf Titelzeile */
|
||
else {
|
||
if(splitState!=='custom'){ splitState='custom'; clearCollapse(); }
|
||
app.style.setProperty('--col', w + 'px');
|
||
}
|
||
} else {
|
||
const h = e.clientY - rect.top, maxh = rect.height - 14;
|
||
if(h <= SNAP) snapTo('a'); /* Diagramm (oben) auf Titelzeile */
|
||
else if(h >= maxh - SNAP) snapTo('b'); /* Editor (unten) auf Titelzeile */
|
||
else {
|
||
if(splitState!=='custom'){ splitState='custom'; clearCollapse(); }
|
||
app.style.setProperty('--drow', h + 'px');
|
||
}
|
||
}
|
||
});
|
||
function endDrag(e){
|
||
if(!dragging) return;
|
||
dragging = false;
|
||
gutter.classList.remove('dragging');
|
||
document.body.style.userSelect = '';
|
||
try{ gutter.releasePointerCapture(e.pointerId); }catch(_){}
|
||
}
|
||
gutter.addEventListener('pointerup', endDrag);
|
||
gutter.addEventListener('pointercancel', endDrag);
|
||
/* Doppelklick auf den Splitter stellt die normale Aufteilung wieder her. */
|
||
gutter.addEventListener('dblclick', () => { splitState = 'normal'; applySplit(); });
|
||
|
||
/* ---------- Zoom für das Diagramm ---------- */
|
||
/* CSS-`zoom` skaliert die Layout-Box, dadurch greifen die Scrollbalken
|
||
des Diagramm-Containers korrekt (anders als transform: scale). */
|
||
const ZMIN = 0.3, ZMAX = 3, ZSTEP = 0.1;
|
||
let zoom = 1;
|
||
function applyZoom(){
|
||
zoom = Math.min(ZMAX, Math.max(ZMIN, Math.round(zoom * 100) / 100));
|
||
out.style.zoom = zoom;
|
||
document.getElementById('zoomReset').textContent = Math.round(zoom * 100) + ' %';
|
||
}
|
||
document.getElementById('zoomIn').addEventListener('click', () => { zoom += ZSTEP; applyZoom(); });
|
||
document.getElementById('zoomOut').addEventListener('click', () => { zoom -= ZSTEP; applyZoom(); });
|
||
document.getElementById('zoomReset').addEventListener('click', () => { zoom = 1; applyZoom(); });
|
||
diagramEl.addEventListener('wheel', e => {
|
||
if(!(e.ctrlKey || e.metaKey)) return; /* Strg/Cmd + Mausrad zoomt */
|
||
e.preventDefault();
|
||
zoom += (e.deltaY < 0 ? ZSTEP : -ZSTEP);
|
||
applyZoom();
|
||
}, {passive:false});
|
||
|
||
const showc = document.getElementById('showc');
|
||
showc.addEventListener('change', render);
|
||
|
||
/* ---------- In die Zwischenablage kopieren ---------- */
|
||
async function writeClipboard(text){
|
||
try{ await navigator.clipboard.writeText(text); return; }catch(_){}
|
||
const ta = document.createElement('textarea'); /* Fallback ohne Clipboard-API */
|
||
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||
document.body.appendChild(ta); ta.select();
|
||
try{ document.execCommand('copy'); }catch(_){}
|
||
document.body.removeChild(ta);
|
||
}
|
||
function flashCopied(btn, restoreTitleKey){
|
||
btn.classList.add('done');
|
||
btn.title = t('copyDone');
|
||
btn.setAttribute('aria-label', t('copyDone'));
|
||
setTimeout(() => {
|
||
btn.classList.remove('done');
|
||
btn.title = t(restoreTitleKey);
|
||
btn.setAttribute('aria-label', t(restoreTitleKey));
|
||
}, 1500);
|
||
}
|
||
document.getElementById('copy').addEventListener('click', async () => {
|
||
await writeClipboard(src.value);
|
||
flashCopied(document.getElementById('copy'), 'copyTooltip');
|
||
});
|
||
document.getElementById('copyDiagram').addEventListener('click', async () => {
|
||
await writeClipboard(treeToText());
|
||
flashCopied(document.getElementById('copyDiagram'), 'copyDiagramTooltip');
|
||
});
|
||
|
||
/* ---------- Internationalisierung (DE/EN/ES/FR) ---------- */
|
||
const I18N = {
|
||
de: {
|
||
subtitle:"Werkbaum – Ein Text-basierter WBS/Projektstrukturplan/Feature-Tree Diagramm-Editor.",
|
||
ghostTooltip:"Ab Größe M sollte ein Element weiter untergliedert werden.",
|
||
discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden",
|
||
fullscreenTooltip:"Vollbild – Panels nutzen die ganze Fensterbreite",
|
||
brandTooltip:"„Werkbaum“ bedeutet so viel wie ‚Werk-Baum‘ — der Baum des Projektstrukturplans (WBS).",
|
||
editorTitle:"Struktur (Text)", diagramTitle:"Diagramm",
|
||
copy:"kopieren", copyDone:"kopiert ✓", copyTooltip:"Text in die Zwischenablage kopieren",
|
||
copyDiagramTooltip:"Diagramm als Textbaum kopieren",
|
||
minimize:"minimieren", normal:"normal", maximize:"maximieren",
|
||
agenda:"Agenda", discarded:"verworfene",
|
||
modeHorizontal:"Horizontal – Organigramm, Diagramm über dem Editor",
|
||
modeKompakt:"Kompakt – alles nach unten, platzsparend",
|
||
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",
|
||
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",
|
||
hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.",
|
||
hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
|
||
hint_root:"Zeile ohne Zeichen = Wurzelknoten. Geschwister sollten dasselbe Zeichen tragen.",
|
||
hint_status:"Status als Kästchen nach dem Zeichen, z. B.",
|
||
hint_size:"Aufwand als T-Shirt-Größe in Klammern, Link einfach als URL anhängen:",
|
||
hint_break:"Ab (M) gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.",
|
||
hint_comment:"Kommentare mit %% — als ganze Zeile oder am Zeilenende.",
|
||
hint_people:"Personen mit @name — erscheinen unten rechts am Knoten."
|
||
},
|
||
en: {
|
||
subtitle:"Werkbaum – A text-based WBS / work breakdown structure / feature-tree diagram editor.",
|
||
ghostTooltip:"From size M upward, an item should be broken down further.",
|
||
discardedTooltip:"Show/hide discarded nodes and their subtree",
|
||
fullscreenTooltip:"Full screen – panels use the full window width",
|
||
brandTooltip:"“Werkbaum” means roughly ‘work tree’ — the tree of the work breakdown structure (WBS).",
|
||
editorTitle:"Structure (text)", diagramTitle:"Diagram",
|
||
copy:"copy", copyDone:"copied ✓", copyTooltip:"Copy text to clipboard",
|
||
copyDiagramTooltip:"Copy diagram as a text tree",
|
||
minimize:"minimize", normal:"normal", maximize:"maximize",
|
||
agenda:"Legend", discarded:"discarded",
|
||
modeHorizontal:"Horizontal – org chart, diagram above the editor",
|
||
modeKompakt:"Compact – everything downward, space-saving",
|
||
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",
|
||
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",
|
||
hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.",
|
||
hint_all:"sub-task, all required", hint_any:"alternative, choose one",
|
||
hint_root:"Line without a marker = root node. Siblings should share the same marker.",
|
||
hint_status:"Status as a checkbox after the marker, e.g.",
|
||
hint_size:"Effort as a T-shirt size in parentheses; add a link simply as a URL:",
|
||
hint_break:"From (M) on: break it down further — if the breakdown is missing, a placeholder appears in the diagram.",
|
||
hint_comment:"Comments with %% — whole line or at the end of a line.",
|
||
hint_people:"People with @name — shown at the bottom-right of the node."
|
||
},
|
||
es: {
|
||
subtitle:"Werkbaum – Un editor de diagramas basado en texto para EDT / estructura de desglose del trabajo / árbol de características.",
|
||
ghostTooltip:"A partir de la talla M, un elemento debería desglosarse más.",
|
||
discardedTooltip:"Mostrar u ocultar los nodos descartados y su subárbol",
|
||
fullscreenTooltip:"Pantalla completa – los paneles usan todo el ancho de la ventana",
|
||
brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo’ — el árbol de la estructura de desglose del trabajo (EDT).",
|
||
editorTitle:"Estructura (texto)", diagramTitle:"Diagrama",
|
||
copy:"copiar", copyDone:"copiado ✓", copyTooltip:"Copiar el texto al portapapeles",
|
||
copyDiagramTooltip:"Copiar el diagrama como árbol de texto",
|
||
minimize:"minimizar", normal:"normal", maximize:"maximizar",
|
||
agenda:"Leyenda", discarded:"descartados",
|
||
modeHorizontal:"Horizontal – organigrama, diagrama sobre el editor",
|
||
modeKompakt:"Compacto – todo hacia abajo, ahorra espacio",
|
||
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",
|
||
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",
|
||
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_root:"Línea sin marcador = nodo raíz. Los hermanos deberían llevar el mismo marcador.",
|
||
hint_status:"Estado como casilla tras el marcador, p. ej.",
|
||
hint_size:"Esfuerzo como talla de camiseta entre paréntesis; añade un enlace simplemente como URL:",
|
||
hint_break:"A partir de (M): sigue desglosando — si falta el desglose, aparece un marcador de posición en el diagrama.",
|
||
hint_comment:"Comentarios con %% — línea completa o al final de la línea.",
|
||
hint_people:"Personas con @nombre — aparecen abajo a la derecha del nodo."
|
||
},
|
||
fr: {
|
||
subtitle:"Werkbaum – Un éditeur de diagrammes basé sur le texte pour WBS / organigramme des tâches / arbre de fonctionnalités.",
|
||
ghostTooltip:"À partir de la taille M, un élément devrait être décomposé davantage.",
|
||
discardedTooltip:"Afficher/masquer les nœuds abandonnés et leur sous-arbre",
|
||
fullscreenTooltip:"Plein écran – les panneaux occupent toute la largeur de la fenêtre",
|
||
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — l’arbre de l’organigramme des tâches (WBS).",
|
||
editorTitle:"Structure (texte)", diagramTitle:"Diagramme",
|
||
copy:"copier", copyDone:"copié ✓", copyTooltip:"Copier le texte dans le presse-papiers",
|
||
copyDiagramTooltip:"Copier le diagramme en arbre de texte",
|
||
minimize:"réduire", normal:"normal", maximize:"agrandir",
|
||
agenda:"Légende", discarded:"abandonnés",
|
||
modeHorizontal:"Horizontal – organigramme, diagramme au-dessus de l'éditeur",
|
||
modeKompakt:"Compact – tout vers le bas, gain de place",
|
||
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",
|
||
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é",
|
||
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_root:"Ligne sans marqueur = nœud racine. Les frères devraient porter le même marqueur.",
|
||
hint_status:"Statut sous forme de case après le marqueur, p. ex.",
|
||
hint_size:"Effort en taille de T-shirt entre parenthèses ; ajoutez un lien simplement comme URL :",
|
||
hint_break:"À partir de (M) : décomposer davantage — si la décomposition manque, un espace réservé apparaît dans le diagramme.",
|
||
hint_comment:"Commentaires avec %% — ligne entière ou en fin de ligne.",
|
||
hint_people:"Personnes avec @nom — affichées en bas à droite du nœud."
|
||
},
|
||
pl: {
|
||
subtitle:"Werkbaum – Edytor diagramów oparty na tekście: WBS / struktura podziału pracy / drzewo funkcji.",
|
||
ghostTooltip:"Od rozmiaru M element powinien być dalej podzielony.",
|
||
discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem",
|
||
fullscreenTooltip:"Pełny ekran – panele wykorzystują całą szerokość okna",
|
||
brandTooltip:"„Werkbaum” znaczy mniej więcej ‚drzewo pracy’ — drzewo struktury podziału pracy (WBS).",
|
||
editorTitle:"Struktura (tekst)", diagramTitle:"Diagram",
|
||
copy:"kopiuj", copyDone:"skopiowano ✓", copyTooltip:"Kopiuj tekst do schowka",
|
||
copyDiagramTooltip:"Kopiuj diagram jako drzewo tekstowe",
|
||
minimize:"minimalizuj", normal:"normalny", maximize:"maksymalizuj",
|
||
agenda:"Legenda", discarded:"odrzucone",
|
||
modeHorizontal:"Poziomo – schemat organizacyjny, diagram nad edytorem",
|
||
modeKompakt:"Kompaktowo – wszystko w dół, oszczędza miejsce",
|
||
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",
|
||
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",
|
||
hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.",
|
||
hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
|
||
hint_root:"Wiersz bez znacznika = węzeł główny. Rodzeństwo powinno mieć ten sam znacznik.",
|
||
hint_status:"Status jako pole wyboru po znaczniku, np.",
|
||
hint_size:"Nakład jako rozmiar koszulki w nawiasach; link dodaj po prostu jako URL:",
|
||
hint_break:"Od (M): dziel dalej — gdy brakuje podziału, w diagramie pojawia się symbol zastępczy.",
|
||
hint_comment:"Komentarze z %% — cały wiersz lub na końcu wiersza.",
|
||
hint_people:"Osoby z @nazwa — pokazywane w prawym dolnym rogu węzła."
|
||
},
|
||
ru: {
|
||
subtitle:"Werkbaum – текстовый редактор диаграмм: СДР / структура декомпозиции работ / дерево функций.",
|
||
ghostTooltip:"Начиная с размера M элемент следует далее декомпозировать.",
|
||
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
|
||
fullscreenTooltip:"Полный экран – панели занимают всю ширину окна",
|
||
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
|
||
editorTitle:"Структура (текст)", diagramTitle:"Диаграмма",
|
||
copy:"копировать", copyDone:"скопировано ✓", copyTooltip:"Скопировать текст в буфер обмена",
|
||
copyDiagramTooltip:"Скопировать диаграмму как текстовое дерево",
|
||
minimize:"свернуть", normal:"обычный", maximize:"развернуть",
|
||
agenda:"Легенда", discarded:"отклонённые",
|
||
modeHorizontal:"Горизонтально – оргсхема, диаграмма над редактором",
|
||
modeKompakt:"Компактно – всё вниз, экономит место",
|
||
modeVertikal:"Вертикально – дерево вправо, диаграмма рядом с редактором",
|
||
zoomOut:"уменьшить", zoomReset:"сбросить", zoomIn:"увеличить",
|
||
zoomAria:"Масштаб (Ctrl/Cmd + колесо мыши)", langMore:"ещё языки",
|
||
empty:"Пока нет структуры — просто начните печатать.", ghost:"Нет декомпозиции",
|
||
mixedWarn:"Строка {line}: под «{label}» смешаны - и | — отображается по первому потомку.",
|
||
st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
|
||
st_fertig:"готово", st_prod:"в эксплуатации", st_verworfen:"отклонено",
|
||
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
||
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
||
hint_root:"Строка без маркера = корневой узел. У соседних узлов должен быть одинаковый маркер.",
|
||
hint_status:"Статус в виде флажка после маркера, напр.",
|
||
hint_size:"Трудоёмкость как размер футболки в скобках; ссылку добавьте просто как URL:",
|
||
hint_break:"С (M): дробите дальше — если декомпозиции нет, в диаграмме появляется заполнитель.",
|
||
hint_comment:"Комментарии через %% — вся строка или в конце строки.",
|
||
hint_people:"Люди через @имя — показываются справа внизу узла."
|
||
},
|
||
hi: {
|
||
subtitle:"Werkbaum – टेक्स्ट-आधारित आरेख संपादक: WBS / कार्य विभाजन संरचना / फ़ीचर-ट्री।",
|
||
ghostTooltip:"आकार M से ऊपर किसी तत्व को और अधिक उप-विभाजित करना चाहिए।",
|
||
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
|
||
fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं",
|
||
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
|
||
editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख",
|
||
copy:"कॉपी करें", copyDone:"कॉपी हो गया ✓", copyTooltip:"टेक्स्ट को क्लिपबोर्ड पर कॉपी करें",
|
||
copyDiagramTooltip:"आरेख को टेक्स्ट-ट्री के रूप में कॉपी करें",
|
||
minimize:"छोटा करें", normal:"सामान्य", maximize:"बड़ा करें",
|
||
agenda:"लेजेंड", discarded:"अस्वीकृत",
|
||
modeHorizontal:"क्षैतिज – संगठन-चार्ट, संपादक के ऊपर आरेख",
|
||
modeKompakt:"सघन – सब नीचे की ओर, जगह बचाता है",
|
||
modeVertikal:"लंबवत – पेड़ दाईं ओर, संपादक के बगल में आरेख",
|
||
zoomOut:"ज़ूम आउट", zoomReset:"रीसेट करें", zoomIn:"ज़ूम इन",
|
||
zoomAria:"ज़ूम (Ctrl/Cmd + माउस-व्हील)", langMore:"और भाषाएँ",
|
||
empty:"अभी कोई संरचना नहीं — बस टाइप करना शुरू करें।", ghost:"विभाजन अनुपस्थित",
|
||
mixedWarn:"पंक्ति {line}: „{label}“ के अंतर्गत - और | मिश्रित हैं — पहले चाइल्ड के अनुसार दिखाया गया।",
|
||
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
|
||
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_verworfen:"अस्वीकृत",
|
||
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
||
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
||
hint_root:"बिना मार्कर वाली पंक्ति = मूल नोड। सहोदर नोड्स का मार्कर समान होना चाहिए।",
|
||
hint_status:"मार्कर के बाद चेकबॉक्स के रूप में स्थिति, जैसे",
|
||
hint_size:"प्रयास कोष्ठक में टी-शर्ट आकार के रूप में; लिंक बस URL के रूप में जोड़ें:",
|
||
hint_break:"(M) से आगे: और विभाजित करें — विभाजन न होने पर आरेख में प्लेसहोल्डर दिखता है।",
|
||
hint_comment:"%% से टिप्पणियाँ — पूरी पंक्ति या पंक्ति के अंत में।",
|
||
hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।"
|
||
}
|
||
};
|
||
let lang = 'de';
|
||
function t(key, vars){
|
||
let s = (I18N[lang] && I18N[lang][key]) ?? I18N.de[key] ?? key;
|
||
if(vars) for(const k in vars) s = s.split('{'+k+'}').join(vars[k]);
|
||
return s;
|
||
}
|
||
function buildHint(){
|
||
const chip = (key, code) => `<span class="chip st-${key}">${code} ${esc(t('st_'+key))}</span>`;
|
||
return `${esc(t('hint_indent'))}<br>
|
||
<code>-</code> ${esc(t('hint_all'))} <em>(all of)</em><br>
|
||
<code class="or-code">|</code> ${esc(t('hint_any'))} <em>(any of)</em><br>
|
||
${esc(t('hint_root'))}<br>
|
||
${esc(t('hint_status'))} <code>- [~] Frontend</code>:
|
||
<div class="chips">
|
||
${chip('idee','[?]')}
|
||
${chip('geplant','[ ]')}
|
||
${chip('arbeit','[~]')}
|
||
${chip('durchstich','[/]')}
|
||
${chip('fertig','[x]')}
|
||
${chip('prod','[^]')}
|
||
${chip('verworfen','[-]')}
|
||
</div>
|
||
${esc(t('hint_size'))}<br>
|
||
<code>- [ ] Backend (L) https://…</code><br>
|
||
${esc(t('hint_break'))}<br>
|
||
${esc(t('hint_comment'))}
|
||
${esc(t('hint_people'))}`;
|
||
}
|
||
function applyLang(l){
|
||
lang = l;
|
||
document.documentElement.lang = l;
|
||
document.querySelectorAll('[data-i18n]').forEach(el => el.textContent = t(el.dataset.i18n));
|
||
document.querySelectorAll('[data-i18n-title]').forEach(el => el.title = t(el.dataset.i18nTitle));
|
||
document.querySelectorAll('[data-i18n-aria]').forEach(el => el.setAttribute('aria-label', t(el.dataset.i18nAria)));
|
||
document.getElementById('hint').innerHTML = buildHint();
|
||
document.querySelectorAll('.langsel button[data-lang]').forEach(b => b.classList.toggle('active', b.dataset.lang===l));
|
||
/* Ist eine aufklappbare Sprache aktiv, den erweiterten Bereich offen halten. */
|
||
if(LANG_EXTRA.includes(l)){
|
||
langsel.classList.add('expanded');
|
||
langMore.setAttribute('aria-expanded', 'true');
|
||
}
|
||
render();
|
||
try{ localStorage.setItem('werkbaum-lang', l); }catch(_){}
|
||
}
|
||
const langsel = document.querySelector('.langsel');
|
||
const langMore = document.getElementById('langMore');
|
||
const LANG_EXTRA = ['pl','ru','hi'];
|
||
langMore.addEventListener('click', () => {
|
||
const exp = langsel.classList.toggle('expanded');
|
||
langMore.setAttribute('aria-expanded', String(exp));
|
||
});
|
||
document.querySelectorAll('.langsel button[data-lang]').forEach(b => {
|
||
b.addEventListener('click', () => applyLang(b.dataset.lang));
|
||
});
|
||
|
||
/* ---------- Vollbild: Panels über die ganze Fensterbreite ---------- */
|
||
const fsBtn = document.getElementById('fsToggle');
|
||
fsBtn.addEventListener('click', () => {
|
||
const on = document.body.classList.toggle('fullscreen');
|
||
fsBtn.classList.toggle('active', on);
|
||
fsBtn.setAttribute('aria-pressed', String(on));
|
||
});
|
||
|
||
applySplit(); /* Startzustand: normal, markiert die normal-Buttons aktiv */
|
||
src.value = INITIAL;
|
||
let startLang = 'de';
|
||
try{ startLang = localStorage.getItem('werkbaum-lang') || 'de'; }catch(_){}
|
||
applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|