diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
index 536087c..753b6ab 100644
--- a/docs/DECISIONS.md
+++ b/docs/DECISIONS.md
@@ -3974,3 +3974,74 @@ Rechnung wie D17-Nachtrag 5.** Nachgemessen statt geschätzt:
Ein `overflow` an der Kopfzeile bleibt weiterhin ausgeschlossen — es klippt die
beiden Aufklapp-Menüs (D50).
+
+## D57 — Das Knoten-Fenster löst den nativen Tooltip überall ab
+Das Fenster aus D52 war für Touch gebaut: Ohne Zeiger gibt es keinen `title`,
+und die Beschreibungen (D40) wären dort gar nicht zu sehen gewesen. Damit
+standen zwei Darstellungen desselben Inhalts nebeneinander — und ausgerechnet
+die schlechtere bekam der Zeiger. Jetzt zeigt das Fenster überall: am Zeiger
+nach kurzer Verzögerung beim Überfahren, bei **Tastaturfokus** sofort, auf
+Touch unverändert beim einfachen Tipp.
+
+**Was der `title` nicht konnte, und zwar prinzipiell:**
+
+- **Keine Absätze.** Die Beschreibungen im mitgelieferten Plan sind bei ~76
+ Zeichen umgebrochen; ein `title` zeigt genau diese harten Umbrüche, in einem
+ schmalen Fenster sah der Text ausgefranst aus. Das Fenster bricht um, wie es
+ die Breite hergibt (D52 hatte das für Touch schon gelöst).
+- **Keine Linie.** Der Trennstrich zwischen Beschreibung und Kurz-Fakten musste
+ aus 24 `─`-Zeichen **gemalt** werden (D40-Nachtrag) — im Fenster ist er eine
+ echte Kante, und ohne Beschreibung entfällt er ganz.
+- **Nichts bei Tastaturfokus.** Kein Browser zeigt einen `title`, wenn man mit
+ Tab auf ein Element springt. Wer das Diagramm mit der Tastatur durchgeht, sah
+ bisher keine Beschreibung. Das ist der eigentliche Zugewinn und kein Beiwerk —
+ nachgemessen mit einem **echten** Tastendruck: Fokus wandert von `#not.line`
+ auf `#not.status`, das Fenster folgt mit dessen Text, 10 px unter dem Knoten,
+ Petrol-Ring am fokussierten Knoten.
+
+**Der Inhalt zieht von `title` nach `data-tip` um.** Bliebe er im `title`,
+zeigte der Browser seinen eigenen Tooltip **zusätzlich** — unterdrücken lässt er
+sich nicht. Es ist kein zweites Attribut, sondern dasselbe unter anderem Namen;
+die Sorge aus D52 („keine 20 kB DOM-Text verdoppeln") bleibt gewahrt.
+
+**Dabei mussten die letzten drei nativen Tooltips *innerhalb* der Knoten
+weichen** — sonst wären sie neben dem Fenster ein zweites Mal erschienen:
+
+- Das **Warndreieck** (`[!]`) trug „High Risk – Aufwand unklar". Das ist der
+ Statusname und steht ohnehin in der Faktenzeile — der `title` war redundant
+ und ist ersatzlos weg.
+- Das **implizite M-Badge** trug die Kostenannahme „Größe fehlt, gilt als M"
+ (D18). Die ist nicht redundant, also wandert sie in die Faktenzeile und der
+ `title` entfällt. Nachgemessen: `data-tip="st_geplant · implicitSizeTooltip ·
+ jumpHint"`, Badge ohne Attribut.
+
+Damit trägt im ganzen Diagramm kein Element mehr einen nativen Tooltip
+(nachgemessen: 0 von 160 Knoten, 0 Kinder) — außer dem Geister-Knoten, der kein
+`.node` ist.
+
+**Absicht-Erkennung statt sofortigem Aufpoppen.** Über einen dichten Baum fährt
+man hinweg, ohne etwas wissen zu wollen: 350 ms Verzögerung. Steht schon ein
+Fenster offen, zeigt der nächste Knoten **ohne** Warten — wer liest, wartet
+nicht noch einmal. Beim Verlassen wird 120 ms gewartet, bevor zugemacht wird:
+Der Weg vom Knoten ins Fenster führt über einen Zwischenraum, in dem der Zeiger
+über keinem von beiden steht. Wer im Fenster ist, hält es offen (Text lässt
+sich markieren).
+
+**Der ↗-Knopf bleibt Touch vorbehalten.** Am Zeiger ist der ganze Knoten der
+Link (§6) — ein Knopf im Fenster wäre ein zweiter Weg zum selben Ziel. Ebenso
+nennt die Faktenzeile nur auf Touch den langen Druck statt Alt+Klick.
+
+**Für Screenreader ändert sich nichts, und das ist Absicht.** Das Fenster ist
+`aria-hidden`: Sein ganzer Inhalt steht bereits im `aria-label` des Knotens
+(SPEC §9), er würde sonst doppelt vorgelesen. Damit dürfen darin keine
+fokussierbaren Elemente liegen — × und ↗ tragen deshalb `tabindex="-1"`;
+erreichbar bleibt beides über den Knoten selbst und über Esc.
+
+**Werkzeuggrenze, wieder dieselbe Sorte:** `element.focus()` setzt in einem
+nicht fokussierten Automatisierungsfenster zwar `document.activeElement`, feuert
+aber **keine** Fokus-Ereignisse (`document.hasFocus() === false`). Der erste
+Prüflauf zeigte deshalb „Tastaturfokus öffnet nichts", obwohl die Logik stimmte;
+mit synthetischem `focusin` öffnete es sofort. Erst das Fronten des Tabs samt
+echtem Klick und echter Tab-Taste hat es bewiesen. Wie D25 (synthetische
+`TouchEvent`s), D17-Nachtrag 4 (Bildschirmtastatur) und D53 (synthetisches
+Strg+Z): Was die Umgebung stellt, stellt der Emulator nicht.
diff --git a/docs/SPEC.md b/docs/SPEC.md
index cc19219..8908da3 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -335,11 +335,12 @@ spiegelt das `"`-Zeichen der Notation. Die Marke erscheint **nicht** im
Grafikexport: Der Text selbst kann dort nicht angezeigt werden, eine Marke
ohne Ziel wäre Rauschen. Siehe D40.
-**Auf Touch-Geräten zeigt der einfache Tipp auf einen Knoten dasselbe als
-Fenster** — ohne Zeiger gibt es keinen Tooltip, die Beschreibung wäre dort sonst
-gar nicht zu sehen. Es ist derselbe Inhalt, nur mit dem, was ein `title` nicht
-kann: Die Beschreibung steht als Absatz, die Kurz-Fakten dahinter durch eine
-echte Linie abgesetzt.
+**Gezeigt wird das immer im eigenen Knoten-Fenster**, nicht im Tooltip des
+Browsers: am Zeiger nach kurzer Verzögerung beim Überfahren, bei Tastaturfokus
+sofort, auf Touch beim einfachen Tipp (§6). Ein `title` kann weder Absätze noch
+eine Linie und erscheint nie beim Tastaturfokus; das Fenster kann beides. Der
+Inhalt ist derselbe — die Beschreibung als Absatz, die Kurz-Fakten dahinter
+durch eine echte Linie abgesetzt. Siehe D57.
- Das Fenster hängt am angetippten Knoten (kleine Spitze zu ihm hin) und weicht
nach oben aus, wenn unten kein Platz ist. Der Knoten trägt, solange es offen
diff --git a/docs/examples/werkbaum.werkbaum b/docs/examples/werkbaum.werkbaum
index c86d05a..26cfa67 100644
--- a/docs/examples/werkbaum.werkbaum
+++ b/docs/examples/werkbaum.werkbaum
@@ -61,6 +61,7 @@
- [^] #ed.a11y: Accessibility: aria labels, focus order (S)
- [^] #ed.mobile: Works on a phone (S)
- [^] #ed.mobile.tip: A tap on a node opens its tooltip as a window (XS) %% no pointer, no tooltip
+ - [x] #ed.tipwindow: The same window at the pointer and on keyboard focus (XS) %% a title has no paragraphs and never shows on focus
- [^] #ed.docs: Several documents in one editor (M)
- [^] #ed.docs.picker: Switcher in the editor title bar (S)
- [^] #ed.docs.url: Load a document from ?sourceUrl= (S)
@@ -462,6 +463,11 @@
at all. A tap opens the same content as a window instead; on a linked node
the link becomes a button inside it, because one tap cannot do both.
+#ed.tipwindow
+ A native title has no paragraphs, cannot draw a line, and never appears on
+ keyboard focus. The window built for touch can do all three, so it now shows
+ everywhere and the title is gone.
+
#ed.docs
Several notation texts side by side, kept in the browser. Each is only text
plus a name — no invented structure format, so this maps onto a server later
diff --git a/frontend/index.html b/frontend/index.html
index 6776c96..3ca57fc 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -256,8 +256,13 @@
Es liegt auf
und ist `position:fixed` — in `#out` hinein würde es
dessen CSS-`zoom` erben (der Text skalierte mit) und von dessen `overflow`
beschnitten (die Falle aus D50). Inhalt wird bei jedem Öffnen ersetzt. -->
-
-
+
+
+
diff --git a/frontend/src/app.js b/frontend/src/app.js
index 3cf7e27..4a8e99e 100644
--- a/frontend/src/app.js
+++ b/frontend/src/app.js
@@ -1246,15 +1246,21 @@ function closeNodeTip(){
function toggleNodeTip(el){
if(tipNode === el){ closeNodeTip(); return; } /* zweiter Tipp schließt */
+ showNodeTip(el, true);
+}
+
+/* `touch` unterscheidet die beiden Anlässe: Am Zeiger ist der ganze Knoten der
+ Link (§6), ein ↗-Knopf wäre dort ein zweiter Weg zum selben Ziel; und der
+ Sprung-Hinweis nennt Alt+Klick, den es auf dem Telefon nicht gibt. */
+function showNodeTip(el, touch){
closeNodeTip();
- const title = el.getAttribute('title') || '';
+ const title = el.getAttribute('data-tip') || '';
const sep = '\n\n' + TIP_RULE + '\n';
const i = title.indexOf(sep);
const desc = i >= 0 ? title.slice(0, i) : '';
- /* Der Sprung-Hinweis nennt im Tooltip Alt+Klick; hier gibt es kein Alt. */
- const facts = (i >= 0 ? title.slice(i + sep.length) : title)
- .replace(t('jumpHint'), t('jumpHintTouch'));
- const href = el.tagName === 'A' ? el.getAttribute('href') : null;
+ const roh = i >= 0 ? title.slice(i + sep.length) : title;
+ const facts = touch ? roh.replace(t('jumpHint'), t('jumpHintTouch')) : roh;
+ const href = touch && el.tagName === 'A' ? el.getAttribute('href') : null;
/* Absätze: Leerzeilen trennen (SPEC §1), einzelne Zeilenumbrüche sind bloß
der Umbruch der Quelldatei und werden zu Leerzeichen. Der `title` kann das
nicht — er zeigt die harten Umbrüche und sah im schmalen Fenster
@@ -1265,7 +1271,7 @@ function toggleNodeTip(el){
nodeTipBody.innerHTML =
(paras.length ? `
${paras.map(p => `
${esc(p)}
`).join('')}
` : '') +
(facts ? `
${esc(facts)}
` : '') +
- (href ? `↗ ${esc(t('tipOpenLink'))}` : '');
+ (href ? `↗ ${esc(t('tipOpenLink'))}` : '');
tipNode = el;
el.classList.add('tipped');
nodeTip.hidden = false;
@@ -1293,6 +1299,53 @@ function placeNodeTip(el){
Math.max(12, Math.min(anchorX - left, w - 12)).toFixed(1) + 'px');
}
+/* ---------- Dasselbe Fenster am Zeiger und an der Tastatur (D57) ----------
+ Es löst den nativen `title` ab: Der konnte weder Absätze noch eine Linie
+ (der Trennstrich war aus 24 `─` gemalt, D40-Nachtrag) und erschien nie beim
+ Tastaturfokus. Beides kann das Fenster ohnehin schon — es wurde nur für
+ Touch gebaut (D52).
+
+ Die Verzögerung ist Absicht-Erkennung: Über einen dichten Baum fährt man
+ hinweg, ohne etwas wissen zu wollen. Beim Wechsel von Knoten zu Knoten
+ zeigt es sofort weiter — wer schon liest, wartet nicht noch einmal. */
+const TIP_DELAY = 350;
+let tipTimer = null, tipLeave = null;
+const finePointer = () => window.matchMedia('(hover:hover) and (pointer:fine)').matches;
+function cancelTipTimers(){
+ clearTimeout(tipTimer); clearTimeout(tipLeave);
+ tipTimer = tipLeave = null;
+}
+out.addEventListener('pointerover', e => {
+ if(e.pointerType === 'touch' || !finePointer()) return;
+ const el = e.target.closest && e.target.closest('.node');
+ if(!el || el === tipNode) return;
+ cancelTipTimers();
+ /* Steht schon eines offen, ist die Absicht erwiesen — dann ohne Warten. */
+ tipTimer = setTimeout(() => showNodeTip(el, false), tipNode ? 0 : TIP_DELAY);
+});
+out.addEventListener('pointerout', e => {
+ if(e.pointerType === 'touch' || !finePointer()) return;
+ clearTimeout(tipTimer);
+ /* Nicht sofort zumachen: Der Weg ins Fenster führt über den Zwischenraum,
+ und dort ist der Zeiger kurz über keinem von beiden. */
+ clearTimeout(tipLeave);
+ tipLeave = setTimeout(() => {
+ if(!nodeTip.matches(':hover')) closeNodeTip();
+ }, 120);
+});
+nodeTip.addEventListener('pointerenter', cancelTipTimers);
+nodeTip.addEventListener('pointerleave', () => { closeNodeTip(); });
+/* Tastaturfokus: ohne Verzögerung — er ist bereits die ausdrückliche Absicht,
+ und ein `title` hat hier noch nie etwas gezeigt. */
+out.addEventListener('focusin', e => {
+ const el = e.target.closest && e.target.closest('.node');
+ if(el && el !== tipNode) showNodeTip(el, false);
+});
+out.addEventListener('focusout', e => {
+ const el = e.target.closest && e.target.closest('.node');
+ if(el && el === tipNode) closeNodeTip();
+});
+
document.getElementById('nodeTipClose').addEventListener('click', closeNodeTip);
/* Tipp/Klick daneben schließt — der Link-Knopf im Fenster aber nicht. */
document.addEventListener('pointerdown', e => {
diff --git a/frontend/src/render.js b/frontend/src/render.js
index 8cccf72..5f20995 100644
--- a/frontend/src/render.js
+++ b/frontend/src/render.js
@@ -131,25 +131,36 @@ function nodeHtml(n, extra, opts, fold){
effKey
? t('heldTooltip', {eff: t('st_' + effKey), own: t('st_' + n.status.key)})
: (n.status ? t('st_' + n.status.key) : ''),
- n.optional ? t('a11yOptional') : '', t('jumpHint')]
+ n.optional ? t('a11yOptional') : '',
+ /* Die Kostenannahme „Größe fehlt, gilt als M" (D18) hatte einen
+ eigenen `title` am Badge — der erschiene neben dem Fenster
+ ein zweites Mal. Sie gehört ohnehin zu den Kurz-Fakten. */
+ (!n.size && cheapPath && !isDone(n)) ? t('implicitSizeTooltip') : '',
+ t('jumpHint')]
.filter(Boolean).join(' · ');
const tip = n.desc && facts ? n.desc + '\n\n' + TIP_RULE + '\n' + facts
: (n.desc || facts);
- const title = ` title="${attr(tip)}"`;
+ /* `data-tip` statt `title`: Der Inhalt wird vom eigenen Knoten-Fenster
+ dargestellt (D57), und ein `title` daneben zeigte der Browser zusätzlich
+ als zweiten, dürftigeren Tooltip. Kein zweites Attribut mit demselben Text
+ — es ist dasselbe, nur unter anderem Namen. */
+ const title = ` data-tip="${attr(tip)}"`;
const tagsHtml = n.tags && n.tags.length
? `${n.tags.map(tag => `${esc(tag)}`).join('')}`
: '';
/* Das implizite M-Badge macht eine KOSTENANNAHME sichtbar (D18). An einem
erledigten Knoten wird keine getroffen — er kostet nichts mehr (D46) —,
dort bleibt es deshalb weg. */
- const implicitTip = attr(t('implicitSizeTooltip'));
+ const implicitSize = !n.size && cheapPath && !isDone(n);
const sizeBadge = n.size
? `${n.size}`
- : (cheapPath && !isDone(n) ? `M` : '');
+ : (implicitSize ? `M` : '');
/* High-Risk: Warndreieck (⚠, Textpräsentation via VS15) an der oberen linken
Ecke. aria-hidden — die Information steckt bereits im Status des aria-label. */
+ /* Kein eigener `title` mehr: Er zeigte sonst zusätzlich zum Knoten-Fenster
+ (D57), und seine Aussage — „High Risk" — steht dort ohnehin als Status. */
const riskMark = n.status && n.status.key === 'highrisk'
- ? `⚠︎`
+ ? `⚠︎`
: '';
/* Falt-Zeichen (SPEC §9/D38): ▾ offen, „▸ n" eingeklappt — das Klickziel
fürs Umklappen (der einfache Klick auf den Knoten bleibt der Link, §6).
diff --git a/frontend/tests/__snapshots__/render.test.js.snap b/frontend/tests/__snapshots__/render.test.js.snap
index e87264c..0b61434 100644
--- a/frontend/tests/__snapshots__/render.test.js.snap
+++ b/frontend/tests/__snapshots__/render.test.js.snap
@@ -1,7 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`renderTreeHtml — kanonisches Beispiel > Grundzustand (Pfad aus, verworfene aus): Struktur-Snapshot 1`] = `"