frontend: Alt-Sprung auffindbar machen (Cursor-Rückmeldung + Legenden-Zeile)

Eine Modifier-Geste, die niemand kennt, ist keine Funktion. Zwei Maßnahmen:

- Solange Alt gedrückt ist, trägt #out die Klasse `alt`: alle Knoten zeigen
  `cursor:alias`, der Knoten unter dem Zeiger einen Petrol-Ring. Das wirkt
  auch auf verlinkten Knoten, wo der einfache Klick dem Link gehört und es
  sonst gar keine Rückmeldung gäbe. Der `blur`-Handler ist Pflicht — bei
  Alt+Tab kommt kein keyup mehr an, der Modus bliebe hängen.
- Die aufklappbare Legende endet mit einer abgesetzten Bedienungs-Zeile
  (`hint_jump` in 9 Sprachen, `.hint-op` mit gestricheltem Trenner), damit
  sie ihre Rolle als Notations-Legende behält.

Verworfen (in D25 samt Begründung festgehalten): einfacher Klick springt auf
Knoten ohne URL (~97 % der Knoten der Beispieldateien) — beiläufig auffindbar,
aber dieselbe Geste täte je nach Knoten Verschiedenes; sowie ein einmaliger
Hinweis nach dem ersten Sprung.

Verifiziert im Browser: ohne Alt Cursor auto bzw. pointer (Link), mit Alt
überall `alias`, nach Loslassen zurück, nach blur ohne keyup (Alt+Tab)
zurückgesetzt; Legenden-Zeile sichtbar und abgesetzt. Vitest 37/37.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-07-27 13:26:43 +02:00
co-authored by Claude Opus 4.8
parent 1f6988b047
commit c496850b88
5 changed files with 68 additions and 11 deletions
+22
View File
@@ -520,3 +520,25 @@ Knoten-Ecken nicht an (die sind laut D18 schon dicht). Die Regel braucht den
any-of-Gruppe (im Test zuerst passiert). Rein visuelle Editierhilfe — im Druck any-of-Gruppe (im Test zuerst passiert). Rein visuelle Editierhilfe — im Druck
abgeschaltet; im Grafikexport erscheint sie ohnehin nicht, weil `diagramToSvg()` abgeschaltet; im Grafikexport erscheint sie ohnehin nicht, weil `diagramToSvg()`
nur Hintergrund, Rahmen, Farbe und Textdekoration ausliest, nie `box-shadow`. nur Hintergrund, Rahmen, Farbe und Textdekoration ausliest, nie `box-shadow`.
**Auffindbarkeit (Nachtrag).** Eine Modifier-Geste, die niemand kennt, ist
keine Funktion. Gegenmaßnahmen, absteigend nach Wirkung:
- **Alt-Modus sichtbar machen:** Solange Alt gedrückt ist, trägt `#out` die
Klasse `alt`; alle Knoten bekommen `cursor:alias` und der Knoten unter dem
Zeiger einen Petrol-Ring. Das wirkt auch auf **verlinkten** Knoten, wo der
einfache Klick dem Link gehört und es sonst gar keine Rückmeldung gäbe. Der
Modus muss zusätzlich am `blur` des Fensters zurückgesetzt werden — bei
Alt+Tab kommt kein `keyup` mehr an und er bliebe hängen.
- **Tooltip an jedem Knoten** (siehe oben, `jumpHint`).
- **Legende:** Die aufklappbare Legende endet mit einer abgesetzten
Bedienungs-Zeile (`hint_jump`, `.hint-op` mit gestricheltem Trenner), damit
sie ihre Rolle als *Notations*-Legende behält.
Erwogen und **verworfen: der einfache Klick springt** (auf Knoten ohne URL, wo
nichts kollidiert — das wären ~97 % der Knoten der Beispieldateien). Hätte die
Geste beiläufig auffindbar gemacht und auf Mobil den langen Druck erspart,
bedeutet aber, dass derselbe Klick je nach Knoten Verschiedenes tut. Ebenfalls
verworfen: ein **einmaliger Hinweis** nach dem ersten Sprung (localStorage-Flag)
— zusätzlicher i18n-Text in 9 Sprachen für einen Effekt, den Cursor und Legende
bereits abdecken.
+5
View File
@@ -240,6 +240,11 @@ verknüpft (siehe D25):
verworfenes Element) heben nichts hervor. verworfenes Element) heben nichts hervor.
- Der **einfache** Klick bleibt unverändert der Link (§6): Ein Knoten mit URL - Der **einfache** Klick bleibt unverändert der Link (§6): Ein Knoten mit URL
öffnet sie weiterhin im neuen Tab. Deshalb Alt und nicht der einfache Klick. öffnet sie weiterhin im neuen Tab. Deshalb Alt und nicht der einfache Klick.
- **Auffindbarkeit:** Solange **Alt gedrückt** ist, zeigen alle Knoten den
Sprung-Cursor und der Knoten unter dem Zeiger einen Petrol-Ring — die Geste
ist im Moment des Ausprobierens sichtbar, auch auf verlinkten Knoten. Dazu
nennt der Knoten-Tooltip die Geste, und die aufklappbare Legende schließt mit
einer Bedienungs-Zeile ab.
- Die Hervorhebung ist eine reine Editierhilfe: nicht im Grafikexport, nicht im - Die Hervorhebung ist eine reine Editierhilfe: nicht im Grafikexport, nicht im
Druck. Druck.
+4 -1
View File
@@ -135,7 +135,10 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
`syncCaret()` setzt die Klasse `current` auf den Knoten der Cursor-Zeile; `syncCaret()` setzt die Klasse `current` auf den Knoten der Cursor-Zeile;
`render()` stellt sie nach jedem Neubau wieder her (ohne zu scrollen). Die `render()` stellt sie nach jedem Neubau wieder her (ohne zu scrollen). Die
CSS-Regel braucht den `#out`-Präfix (`ul.or .node{box-shadow:none}` ist CSS-Regel braucht den `#out`-Präfix (`ul.or .node{box-shadow:none}` ist
spezifischer). spezifischer). Auffindbarkeit: `setAltMode()` setzt bei gedrückter Alt-Taste
die Klasse `alt` an `#out` (Cursor + Ring am Knoten unter dem Zeiger) — der
`blur`-Handler ist Pflicht, sonst bleibt der Modus nach Alt+Tab hängen; dazu
`jumpHint` im Knoten-Tooltip und `hint_jump` als letzte Zeile der Legende.
- Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt - Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt
Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf
Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den
+30 -10
View File
@@ -454,6 +454,16 @@ function syncCaret(){
} }
for(const ev of ['click','keyup','input','focus']) src.addEventListener(ev, syncCaret); for(const ev of ['click','keyup','input','focus']) src.addEventListener(ev, syncCaret);
/* Alt-Modus sichtbar machen: solange Alt gedrückt ist, zeigt jeder Knoten den
Sprung-Cursor und der Knoten unter dem Zeiger einen Petrol-Ring. Das ist die
einzige Rückmeldung, die auch auf verlinkten Knoten funktioniert (dort gehört
der einfache Klick weiterhin dem Link). `blur` ist nötig: bei Alt+Tab kommt
kein keyup mehr an, der Modus bliebe sonst hängen. */
function setAltMode(on){ out.classList.toggle('alt', on); }
window.addEventListener('keydown', e => { if(e.key === 'Alt') setAltMode(true); });
window.addEventListener('keyup', e => { if(e.key === 'Alt' || !e.altKey) setAltMode(false); });
window.addEventListener('blur', () => setAltMode(false));
const app = document.getElementById('app'); const app = document.getElementById('app');
function applyLayout(mode){ function applyLayout(mode){
out.classList.toggle('vertical', mode === 'vertikal'); out.classList.toggle('vertical', mode === 'vertikal');
@@ -756,7 +766,8 @@ const I18N = {
hint_size:"Aufwand als T-Shirt-Größe in Klammern, Link einfach als URL anhängen:", 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_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_comment:"Kommentare mit %% — als ganze Zeile oder am Zeilenende.",
hint_people:"Personen mit @name — erscheinen unten rechts am Knoten." hint_people:"Personen mit @name — erscheinen unten rechts am Knoten.",
hint_jump:"Alt+Klick auf einen Knoten (mobil: langer Druck) springt zur zugehörigen Textzeile."
}, },
en: { en: {
subtitle:"Werkbaum Work Breakdown Structure / Lean Pathfinding · Project structure editor (also feature-tree & requirements)", subtitle:"Werkbaum Work Breakdown Structure / Lean Pathfinding · Project structure editor (also feature-tree & requirements)",
@@ -803,7 +814,8 @@ const I18N = {
hint_size:"Effort as a T-shirt size in parentheses; add a link simply as a URL:", 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_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_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." hint_people:"People with @name — shown at the bottom-right of the node.",
hint_jump:"Alt+click a node (long press on touch) jumps to its line in the text."
}, },
es: { es: {
subtitle:"Werkbaum EDT / Lean Pathfinding · Editor de estructura de proyectos (también árboles de características y requisitos)", subtitle:"Werkbaum EDT / Lean Pathfinding · Editor de estructura de proyectos (también árboles de características y requisitos)",
@@ -850,7 +862,8 @@ const I18N = {
hint_size:"Esfuerzo como talla de camiseta entre paréntesis; añade un enlace simplemente como URL:", 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_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_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." hint_people:"Personas con @nombre — aparecen abajo a la derecha del nodo.",
hint_jump:"Alt+clic en un nodo (pulsación larga en táctil) salta a su línea en el texto."
}, },
fr: { fr: {
subtitle:"Werkbaum WBS / Lean Pathfinding · Éditeur de structure de projet (aussi pour arbres de fonctionnalités et d'exigences)", subtitle:"Werkbaum WBS / Lean Pathfinding · Éditeur de structure de projet (aussi pour arbres de fonctionnalités et d'exigences)",
@@ -897,7 +910,8 @@ const I18N = {
hint_size:"Effort en taille de T-shirt entre parenthèses ; ajoutez un lien simplement comme URL :", 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_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_comment:"Commentaires avec %% — ligne entière ou en fin de ligne.",
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_jump:"Alt+clic sur un nœud (appui long sur tactile) saute à sa ligne dans le texte."
}, },
pl: { pl: {
subtitle:"Werkbaum WBS / Lean Pathfinding · Edytor struktury projektów (również dla drzew funkcji i wymagań)", subtitle:"Werkbaum WBS / Lean Pathfinding · Edytor struktury projektów (również dla drzew funkcji i wymagań)",
@@ -944,7 +958,8 @@ const I18N = {
hint_size:"Nakład jako rozmiar koszulki w nawiasach; link dodaj po prostu jako URL:", 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_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_comment:"Komentarze z %% — cały wiersz lub na końcu wiersza.",
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_jump:"Alt+kliknięcie węzła (długie naciśnięcie na dotyku) przechodzi do jego wiersza w tekście."
}, },
ru: { ru: {
subtitle:"Werkbaum СДР / Lean Pathfinding · Редактор структуры проектов (также для деревьев функций и требований)", subtitle:"Werkbaum СДР / Lean Pathfinding · Редактор структуры проектов (также для деревьев функций и требований)",
@@ -991,7 +1006,8 @@ const I18N = {
hint_size:"Трудоёмкость как размер футболки в скобках; ссылку добавьте просто как URL:", hint_size:"Трудоёмкость как размер футболки в скобках; ссылку добавьте просто как URL:",
hint_break:"С (M): дробите дальше — если декомпозиции нет, в диаграмме появляется заполнитель.", hint_break:"С (M): дробите дальше — если декомпозиции нет, в диаграмме появляется заполнитель.",
hint_comment:"Комментарии через %% — вся строка или в конце строки.", hint_comment:"Комментарии через %% — вся строка или в конце строки.",
hint_people:"Люди через @имя — показываются справа внизу узла." hint_people:"Люди через @имя — показываются справа внизу узла.",
hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте."
}, },
hi: { hi: {
subtitle:"Werkbaum WBS / Lean Pathfinding · परियोजना संरचना संपादक (फ़ीचर और रिक्वायरमेंट ट्री के लिए भी)", subtitle:"Werkbaum WBS / Lean Pathfinding · परियोजना संरचना संपादक (फ़ीचर और रिक्वायरमेंट ट्री के लिए भी)",
@@ -1038,7 +1054,8 @@ const I18N = {
hint_size:"प्रयास कोष्ठक में टी-शर्ट आकार के रूप में; लिंक बस URL के रूप में जोड़ें:", hint_size:"प्रयास कोष्ठक में टी-शर्ट आकार के रूप में; लिंक बस URL के रूप में जोड़ें:",
hint_break:"(M) से आगे: और विभाजित करें — विभाजन न होने पर आरेख में प्लेसहोल्डर दिखता है।", hint_break:"(M) से आगे: और विभाजित करें — विभाजन न होने पर आरेख में प्लेसहोल्डर दिखता है।",
hint_comment:"%% से टिप्पणियाँ — पूरी पंक्ति या पंक्ति के अंत में।", hint_comment:"%% से टिप्पणियाँ — पूरी पंक्ति या पंक्ति के अंत में।",
hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।" hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।",
hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है।"
}, },
zh: { zh: {
subtitle:"Werkbaum WBS / Lean Pathfinding · 项目结构编辑器(也支持功能树和需求树)", subtitle:"Werkbaum WBS / Lean Pathfinding · 项目结构编辑器(也支持功能树和需求树)",
@@ -1085,7 +1102,8 @@ const I18N = {
hint_size:"用括号中的 T 恤尺码表示工作量;链接直接作为 URL 附加:", hint_size:"用括号中的 T 恤尺码表示工作量;链接直接作为 URL 附加:",
hint_break:"从 (M) 起:继续细分——若缺少细分,图表中会出现占位符。", hint_break:"从 (M) 起:继续细分——若缺少细分,图表中会出现占位符。",
hint_comment:"用 %% 注释——整行或行尾。", hint_comment:"用 %% 注释——整行或行尾。",
hint_people:"用 @姓名 表示人员——显示在节点右下角。" hint_people:"用 @姓名 表示人员——显示在节点右下角。",
hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行。"
}, },
ja: { ja: {
subtitle:"Werkbaum WBS / Lean Pathfinding · プロジェクト構造エディター(フィーチャーツリーと要件ツリーにも対応)", subtitle:"Werkbaum WBS / Lean Pathfinding · プロジェクト構造エディター(フィーチャーツリーと要件ツリーにも対応)",
@@ -1132,7 +1150,8 @@ const I18N = {
hint_size:"工数は括弧内の T シャツサイズで;リンクは URL としてそのまま追加:", hint_size:"工数は括弧内の T シャツサイズで;リンクは URL としてそのまま追加:",
hint_break:"(M) 以上:さらに分解 — 分解がないと図にプレースホルダーが表示されます。", hint_break:"(M) 以上:さらに分解 — 分解がないと図にプレースホルダーが表示されます。",
hint_comment:"%% でコメント — 行全体または行末。", hint_comment:"%% でコメント — 行全体または行末。",
hint_people:"@名前 で担当者 — ノードの右下に表示されます。" hint_people:"@名前 で担当者 — ノードの右下に表示されます。",
hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。"
} }
}; };
let lang = 'de'; let lang = 'de';
@@ -1161,7 +1180,8 @@ function buildHint(){
<code>- [ ] Backend (L) https://…</code><br> <code>- [ ] Backend (L) https://…</code><br>
${esc(t('hint_break'))}<br> ${esc(t('hint_break'))}<br>
${esc(t('hint_comment'))} ${esc(t('hint_comment'))}
${esc(t('hint_people'))}`; ${esc(t('hint_people'))}
<div class="hint-op">${esc(t('hint_jump'))}</div>`;
} }
function applyLang(l){ function applyLang(l){
lang = l; lang = l;
+7
View File
@@ -373,6 +373,8 @@
padding:12px 16px;border-top:1px dashed rgba(36,52,71,.18); padding:12px 16px;border-top:1px dashed rgba(36,52,71,.18);
font-size:.8rem;color:var(--muted);line-height:1.7; font-size:.8rem;color:var(--muted);line-height:1.7;
} }
/* Bedienung statt Notation — abgesetzt, damit die Legende ihre Rolle behält. */
.hint-op{margin-top:8px;padding-top:8px;border-top:1px dashed rgba(36,52,71,.18)}
.hint code{ .hint code{
font-family:'IBM Plex Mono',monospace;font-size:.75rem; font-family:'IBM Plex Mono',monospace;font-size:.75rem;
background:var(--paper);border:1px solid rgba(36,52,71,.12); background:var(--paper);border:1px solid rgba(36,52,71,.12);
@@ -441,6 +443,11 @@
(weiter unten) ist spezifischer als `.node.current` und schluckte den Ring (weiter unten) ist spezifischer als `.node.current` und schluckte den Ring
sonst überall unterhalb einer any-of-Gruppe. */ sonst überall unterhalb einer any-of-Gruppe. */
#out .node.current{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--ink)} #out .node.current{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--ink)}
/* Alt gedrückt = Sprung-Geste scharf: Cursor an allen Knoten, Petrol-Ring am
Knoten unter dem Zeiger. Auf verlinkten Knoten ist das die einzige
Rückmeldung, weil dort der einfache Klick dem Link gehört (D25). */
#out.alt .node{cursor:alias}
#out.alt .node:hover{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--or)}
/* Auf Touch ist der lange Druck die Sprung-Geste — kein iOS-Link-Callout. */ /* Auf Touch ist der lange Druck die Sprung-Geste — kein iOS-Link-Callout. */
a.node{-webkit-touch-callout:none} a.node{-webkit-touch-callout:none}
.ext{font-size:.72em;margin-left:5px;opacity:.55} .ext{font-size:.72em;margin-left:5px;opacity:.55}