diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 580d119..b1ee742 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -3347,3 +3347,105 @@ synthetisches `.blur()` löst im Automaten keinen aus. Gegenprobe am ebenfalls nichts, es ist also die Werkzeuggrenze und keine Regression. Dieselbe Lehre wie in D25 und D17-Nachtrag 4 — was die Geräteumgebung stellt, beweist der Emulator nicht. + +## D52 — Auf Touch öffnet der einfache Tipp das Knoten-Fenster, nicht den Link +Die Beschreibungen (D40) lebten im `title` — und ein `title` braucht einen +Zeiger. D40 hat das als bekannte Grenze notiert („auf Touch-Geräten gibt es +keine Tooltips — dort bleibt der Text vorerst nur im `aria-label`; ein Pop-up +kann später ergänzt werden"). Auf dem Telefon war die Beschreibung damit **gar +nicht** zu sehen, und seit D48 hängt an jedem der 172 Plan-Knoten eine. + +**Die eine echte Frage war der Link.** Ein Knoten mit URL ist ein `` und +belegt den einfachen Klick vollständig (§6, D6); ein Tipp kann nicht beides +tun. Entschieden (Nutzer): **Auf Touch öffnet der Tipp immer das Fenster, und +der Link steht darin als Knopf.** Damit hat die Geste dort **eine** Bedeutung — +„ansehen" —, und die Tooltips sind auf **allen** Knoten erreichbar. Preis: +SPEC §6 bekommt eine Touch-Ausnahme, und der Link kostet einen zweiten Tipp. + +**Verworfen:** +- **Nur unverlinkte Knoten** — hätte §6 unangetastet gelassen, aber derselbe + Tipp hätte je nach Knoten Verschiedenes getan. Genau das hat D25 für diese + Geste ausdrücklich abgelehnt, und ausgerechnet die verlinkten Knoten (oft die + interessanten) wären ohne Beschreibung geblieben. +- **Erster Tipp Fenster, zweiter Tipp Link** — spart den Knopf, führt aber eine + unsichtbare Zusatzregel ein: Nichts zeigt an, dass ein zweiter Tipp etwas + anderes tut. + +**Es hängt an den Touch-Ereignissen, nicht an einer Media Query.** Damit +verhält sich ein Gerät mit **beidem** (Touch-Notebook) richtig, ohne +Sonderfall: Der Finger öffnet das Fenster, die Maus öffnet weiterhin den Link +und zeigt den Tooltip. Nachgemessen am Schreibtisch: Klick auf den verlinkten +Wurzelknoten öffnet den Link, das Fenster bleibt zu, der `title` steht +unverändert am Knoten. + +**Die drei Touch-Gesten unterscheiden sich am vorhandenen Zustand, ohne neues +Merkerfeld.** Der lange Druck (D25) setzt nach 500 ms `armedEl`, und jedes +`touchmove` räumt den Timer weg. Bei `touchend` gilt also: `armedEl` gesetzt ⇒ +Sprung; Timer läuft noch ⇒ kurzer Tipp ohne Wischen ⇒ Fenster; beides weg ⇒ es +wurde gescrollt ⇒ nichts. **Ausgenommen ist das Falt-Zeichen** (D38): Der Tipp +darauf muss weiter umklappen, und weil das Fenster `preventDefault()` braucht +(sonst öffnete der Link-Knoten zusätzlich seine URL), hätte es den folgenden +Klick sonst verschluckt — Falten wäre auf Touch unbedienbar geworden. +Nachgemessen: Tipp auf das ▾ des Wurzelknotens 149 → 1 Knoten, kein Fenster. + +**Es ist ein Fenster, kein zweiter Tooltip — also nutzt es, was ein `title` +nicht kann.** Der Trennstrich zwischen Beschreibung und Kurz-Fakten musste +dort aus 24 `─` gebaut werden (D40-Nachtrag), weil `title` kein Markup kennt; +hier ist er eine echte Linie, und ohne Beschreibung entfällt er ganz. Zerlegt +wird der `title` **an genau diesem Strich** (`TIP_RULE`, jetzt exportiert). Die +Alternative wäre ein zweites data-Attribut mit derselben Beschreibung gewesen — +im Werkbaum-Plan rund 20 kB DOM-Text als reine Verdopplung. + +**Einzelne Zeilenumbrüche werden zu Leerzeichen, Leerzeilen zu Absätzen.** Das +ist die Lesart von SPEC §1 („Leerzeilen bleiben als Absatztrenner"), und es war +im Fenster sofort zu sehen: Die Beschreibungen im Plan sind bei ~76 Zeichen +umgebrochen, und in einem 336 px breiten Fenster stand der Text dadurch +ausgefranst („…a textual notation for work breakdown / structures, a browser +editor…"). Der `title` zeigt die harten Umbrüche weiterhin — dort haben wir +keine Wahl. + +**Der Sprung-Hinweis nennt hier den langen Druck.** Der Tooltip endet mit +„Alt+Klick: zur Zeile im Text"; Alt gibt es auf dem Telefon nicht. Neuer +i18n-Schlüssel `jumpHintTouch` in allen neun Sprachen, plus `tipClose` und +`tipOpenLink`. + +**`position:fixed` auf ``, nicht in `#out`.** Ein Kind von `#out` erbte +dessen CSS-`zoom` (der Fenstertext skalierte mit dem Diagramm) und würde von +dessen `overflow` beschnitten — die Falle aus D50. Gesetzt wird aus +`getBoundingClientRect()`, das den Zoom schon enthält; waagerecht an den +Fensterrand geklemmt, nach oben ausweichend, wenn unten kein Platz ist. Die +Spitze bleibt über `--tipx` am Knoten, auch wenn geklemmt wurde (nachgemessen: +0 px Abweichung von der Knotenmitte im Normalfall). + +**Zu macht es alles, was seine Aussage hinfällig macht:** Tipp daneben, zweiter +Tipp auf denselben Knoten, Esc, das ×, **Scrollen des Diagramms** (das Fenster +ist `fixed`, der Knoten wandert — es zeigte danach auf etwas anderes), der +**Bereichswechsel** auf Mobil (das Ziel ist dann `display:none`), der **Sprung +in den Text** (er führt weg) und jeder **Neubau** (er ersetzt das Element, an +dem es hängt). Die letzten drei fehlten in der ersten Fassung und fielen erst +im Durchspielen auf. + +**Nachgemessen** (375 × 812, mitgelieferter Werkbaum-Plan): Tipp auf den +Wurzelknoten öffnet 336 × 220 px vollständig im Bild, mit Beschreibung als +einem Absatz ohne harte Umbrüche, Fakten `#wb · in Arbeit · Langer Druck: zur +Zeile im Text` und Knopf „↗ Link öffnen"; der Knoten trägt den Petrol-Ring, die +URL wird **nicht** geöffnet. Knoten ohne Beschreibung: nur die Faktenzeile, +39 px, ohne Trennstrich. Zweiter Tipp, Esc, Tipp daneben und eine **echte** +Wischgeste (`scrollLeft` 0 → 300) schließen; der lange Druck springt und +schließt mit. + +**Werkzeuggrenze, wie in D25 und D17-Nachtrag 4:** Synthetische `TouchEvent`s +beweisen nur die eigene Ereignis-Logik — dass der Tipp den Link unterdrückt, +dass die drei Gesten auseinandergehalten werden, dass Position und Inhalt +stimmen. Sie beweisen **nicht**, wie ein echter Finger mit dem 300-ms-Klick, +dem Doppeltipp-Zoom und der Textauswahl des Systems zusammenspielt. Zwei +Messungen liefen deshalb auffällig ins Leere und sind hier festgehalten, damit +sie niemand für Befunde nimmt: Ein programmatisch gesetztes `scrollLeft` löste +**kein** `scroll`-Ereignis aus (die D33-Falle — geprüft wurde daraufhin mit +einer echten Wischgeste), und ein synthetischer Tipp trifft einen Knoten auch +dann, wenn er 8968 px **außerhalb** des Bildes liegt — dort zeigte das Fenster +folgerichtig auf nichts. Ein Finger kann das nicht. + +**Nicht im Druck** (`.nodetip` und der `.tipped`-Ring ausgeblendet) und nicht +im Grafikexport — Bedienung, keine Aussage über den Plan; der Export liest +ohnehin nur `#out`. diff --git a/docs/SPEC.md b/docs/SPEC.md index b7a70d5..de3387c 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -286,6 +286,11 @@ effektiv nicht weiter sein als das, was er braucht. - Ein nacktes `https://…`-Token macht den ganzen Knoten klickbar (neuer Tab, `rel="noopener"`); Kennzeichnung mit ↗ hinter dem Label. +- **Ausnahme auf Touch-Geräten:** Dort öffnet der einfache Tipp nicht die URL, + sondern das Knoten-Fenster (§9) — der Link steht darin als eigener Knopf. Ohne + Zeiger gibt es keinen Tooltip, und ein einziger Tipp kann nicht beides tun; + „ansehen" ist die häufigere Absicht, und der Link bleibt einen Tipp entfernt. + Siehe D52. ## 7. Personen-Tags @@ -330,6 +335,26 @@ 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. + +- 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 + ist, einen Petrol-Ring. +- Es schließt beim Tipp daneben, beim erneuten Tipp auf denselben Knoten, mit + **Esc**, über sein ×, beim Scrollen des Diagramms und bei jedem Neubau. +- Ein **verlinkter** Knoten (§6) bekommt darin einen ↗-Knopf, der die URL im + neuen Tab öffnet — auf Touch ist das der Weg zum Link. +- Der **Sprung-Hinweis** nennt hier den langen Druck statt Alt+Klick (§9, + Sprung zwischen Diagramm und Text) — Alt gibt es auf dem Telefon nicht. +- Reine Bedienhilfe: nicht im Grafikexport, nicht im Druck. Auf Geräten mit + Zeiger ändert sich nichts, dort bleibt es beim Tooltip. + +Siehe D52. + **Die Knotenfarbe zeigt den effektiven Status (§4)**, nicht den intrinsischen — das Diagramm beantwortet „wie weit ist das wirklich?“. Wo der eigene Status **weiter** ist als der effektive (der Knoten wird von Abhängigkeiten diff --git a/docs/examples/werkbaum.werkbaum b/docs/examples/werkbaum.werkbaum index 6826b82..1765751 100644 --- a/docs/examples/werkbaum.werkbaum +++ b/docs/examples/werkbaum.werkbaum @@ -59,6 +59,7 @@ - [^] #ed.i18n.auto: Default taken from the browser (XS) - [^] #ed.a11y: Accessibility: aria labels, focus order (S) - [^] #ed.mobile: Works on a phone (S) + - [x] #ed.mobile.tip: A tap on a node opens its tooltip as a window (XS) %% no pointer, no tooltip - [^] #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) @@ -441,6 +442,11 @@ Below 640 px exactly one area is visible, diagram or text, and a button switches between them. Any split would leave both halves too small to use. +#ed.mobile.tip + A tooltip needs a pointer, so without one the description could not be seen + 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.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/CLAUDE.md b/frontend/CLAUDE.md index 6871717..cf9f608 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -532,3 +532,16 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der (das einzige Element ohne feste Größe) auf einen 2-px-Strich, während sein Icon darüber hinausragt. Das sieht aus wie ein Trennstrich und ist unbenutzbar. +- **Die drei Touch-Gesten am Knoten (D25, D38, D52)** teilen sich `touchstart`/ + `touchend` und werden am vorhandenen Zustand unterschieden: `armedEl` gesetzt + (500 ms um) ⇒ Sprung, Timer läuft noch ⇒ kurzer Tipp ⇒ Knoten-Fenster, beides + weg (jedes `touchmove` räumt auf) ⇒ es wurde gescrollt ⇒ nichts. Kein eigenes + Merkerfeld nötig. Wer hier eine vierte Geste einhängt, denkt an das + **Falt-Zeichen**: Es ist ausgenommen, weil das Fenster `preventDefault()` + braucht (sonst öffnet ein Link-Knoten zusätzlich seine URL) — und das + verschluckt den Klick, mit dem gefaltet wird. +- **Das Knoten-Fenster liegt auf ``, nicht in `#out` (D52):** In `#out` + erbte es dessen CSS-`zoom` und würde von dessen `overflow` beschnitten (D50). + Es schließt bei allem, was seine Aussage hinfällig macht — Tipp daneben, + zweiter Tipp, Esc, ×, Diagramm-Scroll, Bereichswechsel, Sprung in den Text, + `render()`. Die letzten drei sind beim Bauen zuerst vergessen worden. diff --git a/frontend/index.html b/frontend/index.html index d8dc943..6979134 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -229,6 +229,16 @@ + + + diff --git a/frontend/src/app.js b/frontend/src/app.js index 548c178..6663805 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -1,7 +1,7 @@ import './style.css'; import { parse, setFoldMark } from './parser.js'; import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM } from './model.js'; -import { esc, renderTreeHtml } from './render.js'; +import { esc, renderTreeHtml, TIP_RULE } from './render.js'; import { formatWarning } from './warnings.js'; import { padUrls } from './remote.js'; /* Werkbaum, mit Werkbaum geplant — als mitgeliefertes Dokument „Werkbank" (D27). @@ -84,6 +84,9 @@ let foldOverrides = new Map(), foldByLine = new Map(); parse -> Wurzeln filtern (verworfene) -> günstigen Pfad markieren -> render.js baut den HTML-String -> in #out schreiben -> Pfadlinie zeichnen. */ function render(){ + /* Das Knoten-Fenster (D52) hängt an einem Element, das der Neubau ersetzt — + und seine Position ist ohnehin gemessen, also gleich hinfällig. */ + closeNodeTip(); const parsed = parse(src.value); let roots = parsed.roots; const showDiscarded = discardedShown(); @@ -865,6 +868,7 @@ src.addEventListener('pointerdown', () => keyboardOnJump(false)); function jumpToLine(line){ const r = lineRange(line); if(!r) return; + closeNodeTip(); /* der Sprung führt weg vom Knoten (D52) */ revealEditor(); keyboardOnJump(true); src.focus({preventScroll: true}); @@ -1096,15 +1100,20 @@ out.addEventListener('keydown', e => { der Zielknoten bekommt den Petrol-Ring („scharf"). Der folgende Klick wird unterdrückt, sonst öffnete ein Link-Knoten zusätzlich seine URL; das Kontextmenü/Callout ebenso (die Geste ist hier vergeben). */ -let pressTimer = null, armedEl = null; +let pressTimer = null, armedEl = null, pressNode = null; function disarmPress(){ if(pressTimer){ clearTimeout(pressTimer); pressTimer = null; } if(armedEl){ armedEl.classList.remove('armed'); armedEl = null; } + pressNode = null; } out.addEventListener('touchstart', e => { disarmPress(); const el = nodeFromEvent(e); if(!el) return; + /* Ein Tipp auf das Falt-Zeichen klappt um (D38) — das darf der kurze Tipp + nicht abfangen, sonst wäre Falten auf Touch nicht mehr zu bedienen. */ + if(e.target && e.target.closest && e.target.closest('.fold')) return; + pressNode = el; pressTimer = setTimeout(() => { pressTimer = null; armedEl = el; @@ -1114,14 +1123,106 @@ out.addEventListener('touchstart', e => { out.addEventListener('touchmove', disarmPress, {passive: true}); out.addEventListener('touchend', e => { const el = armedEl; + /* Kurzer Tipp: der Timer läuft noch (nach 500 ms wäre `armedEl` gesetzt) und + es wurde nicht gewischt (jedes `touchmove` räumt den Timer weg). Der + Zustand unterscheidet die drei Gesten also ohne eigenes Merkerfeld. */ + const tap = !el && pressTimer && pressNode; + const tapped = pressNode; disarmPress(); - if(!el) return; - e.preventDefault(); /* unterdrückt den nachfolgenden Klick/Link */ - jumpToLine(+el.dataset.line); /* echte Nutzergeste -> der Fokus bleibt */ + if(el){ + e.preventDefault(); /* unterdrückt den nachfolgenden Klick/Link */ + jumpToLine(+el.dataset.line); /* echte Nutzergeste -> der Fokus bleibt */ + return; + } + if(!tap) return; + /* Der einfache Tipp zeigt den Tooltip als Fenster (SPEC §6/§9, D52). Ohne + `preventDefault()` öffnete ein Link-Knoten zusätzlich seine URL — auf Touch + ist der Link stattdessen ein Knopf IM Fenster. */ + e.preventDefault(); + toggleNodeTip(tapped); }, {passive: false}); out.addEventListener('touchcancel', disarmPress); out.addEventListener('contextmenu', e => { if(pressTimer || armedEl) e.preventDefault(); }); +/* ---------- Knoten-Fenster auf Touch (SPEC §6/§9, D52) ---------- + Ein `title` braucht einen Zeiger; ohne Zeiger wäre die Beschreibung (D40) gar + nicht zu sehen. Das Fenster zeigt denselben Inhalt, zerlegt am Trennstrich + des Tooltips (TIP_RULE) in Beschreibung und Kurz-Fakten — dort, wo der + Tooltip 24 `─` malen musste, steht hier eine echte Linie. Kein zweites + data-Attribut mit derselben Beschreibung: Das verdoppelte im Werkbaum-Plan + rund 20 kB DOM-Text für nichts. */ +const nodeTip = document.getElementById('nodeTip'); +const nodeTipBody = document.getElementById('nodeTipBody'); +let tipNode = null; + +function closeNodeTip(){ + if(!tipNode) return; + tipNode.classList.remove('tipped'); + tipNode = null; + nodeTip.hidden = true; +} + +function toggleNodeTip(el){ + if(tipNode === el){ closeNodeTip(); return; } /* zweiter Tipp schließt */ + closeNodeTip(); + const title = el.getAttribute('title') || ''; + 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; + /* 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 + ausgefranst aus (im Werkbaum-Plan bricht jede Beschreibung bei ~76 + Zeichen). Hier wird umgebrochen, wie es das Fenster braucht. */ + const paras = desc.split(/\n{2,}/) + .map(p => p.replace(/\s*\n\s*/g, ' ').trim()).filter(Boolean); + nodeTipBody.innerHTML = + (paras.length ? `
${paras.map(p => `

${esc(p)}

`).join('')}
` : '') + + (facts ? `
${esc(facts)}
` : '') + + (href ? `↗ ${esc(t('tipOpenLink'))}` : ''); + tipNode = el; + el.classList.add('tipped'); + nodeTip.hidden = false; + placeNodeTip(el); +} + +/* Setzt das Fenster unter den Knoten, bei Platzmangel darüber; waagerecht auf + die Knotenmitte, geklemmt an den Fensterrand. Die Spitze bleibt über + `--tipx` am Knoten, auch wenn das Fenster geklemmt wurde. */ +function placeNodeTip(el){ + const b = el.getBoundingClientRect(); + const gap = 10, pad = 8; + nodeTip.classList.remove('above'); + nodeTip.style.left = '0px'; /* erst messen, dann setzen */ + nodeTip.style.top = '0px'; + const w = nodeTip.offsetWidth, h = nodeTip.offsetHeight; + const above = b.bottom + gap + h > window.innerHeight && b.top - gap - h > 0; + if(above) nodeTip.classList.add('above'); + const anchorX = b.left + b.width / 2; + const left = Math.max(pad, Math.min(anchorX - w / 2, window.innerWidth - w - pad)); + nodeTip.style.left = left + 'px'; + nodeTip.style.top = (above ? b.top - gap - h : b.bottom + gap) + 'px'; + /* Spitze relativ zum Fenster, aber innerhalb seiner Rundungen gehalten. */ + nodeTip.style.setProperty('--tipx', + Math.max(12, Math.min(anchorX - left, w - 12)).toFixed(1) + 'px'); +} + +document.getElementById('nodeTipClose').addEventListener('click', closeNodeTip); +/* Tipp/Klick daneben schließt — der Link-Knopf im Fenster aber nicht. */ +document.addEventListener('pointerdown', e => { + if(!tipNode || nodeTip.contains(e.target)) return; + if(e.target && e.target.closest && e.target.closest('.node') === tipNode) return; + closeNodeTip(); +}, true); +document.addEventListener('keydown', e => { if(e.key === 'Escape') closeNodeTip(); }); +/* Beim Scrollen des Diagramms wandert der Knoten, das `position:fixed`-Fenster + nicht — es zeigte dann auf etwas anderes. Also zumachen. */ +document.querySelector('.diagram').addEventListener('scroll', closeNodeTip, {passive: true}); + /* Welcher Knoten gehört zu einer Textzeile? Zuerst der Knoten, DER auf dieser Zeile steht; sonst der Knoten, dessen **Beschreibung** hier steht (SPEC §9): Eine `"`-Zeile und die Zeilen eines `---`-Blocks tragen keinen eigenen @@ -1265,6 +1366,9 @@ function applyMobilePane(){ document.body.classList.toggle('pane-text', mobilePane === 'text'); } function setMobilePane(pane, save){ + /* Das Knoten-Fenster (D52) zeigt auf einen Knoten im Diagramm — wechselt der + Bereich, ist das Ziel weg (`display:none`). */ + closeNodeTip(); mobilePane = pane; applyMobilePane(); /* Ein Panel mit `display:none` misst sich zu **null**. Alles, was aus der @@ -1552,6 +1656,10 @@ const I18N = { paneToDiagram:"Zum Diagramm wechseln", ghostTooltip:"Ab Größe M sollte ein Element weiter untergliedert werden.", jumpHint:"Alt+Klick: zur Zeile im Text", + /* Auf Touch nennt das Knoten-Fenster (D52) den langen Druck — Alt gibt es dort nicht. */ + jumpHintTouch:"Langer Druck: zur Zeile im Text", + tipClose:"Schließen", + tipOpenLink:"Link öffnen", padReadonly:"Wird im Pad bearbeitet — hier nur lesen.", padEdit:"Pad zum Bearbeiten öffnen", padRefresh:"Vom Pad neu laden", @@ -1634,6 +1742,9 @@ const I18N = { paneToDiagram:"Switch to the diagram", ghostTooltip:"From size M upward, an item should be broken down further.", jumpHint:"Alt+click: jump to the line in the text", + jumpHintTouch:"Long press: jump to the line in the text", + tipClose:"Close", + tipOpenLink:"Open link", padReadonly:"Edited in the pad — read-only here.", padEdit:"Open the pad to edit", padRefresh:"Reload from the pad", @@ -1716,6 +1827,9 @@ const I18N = { paneToDiagram:"Cambiar al diagrama", ghostTooltip:"A partir de la talla M, un elemento debería desglosarse más.", jumpHint:"Alt+clic: ir a la línea en el texto", + jumpHintTouch:"Pulsación larga: ir a la línea en el texto", + tipClose:"Cerrar", + tipOpenLink:"Abrir enlace", padReadonly:"Se edita en el pad — aquí solo lectura.", padEdit:"Abrir el pad para editar", padRefresh:"Recargar desde el pad", @@ -1798,6 +1912,9 @@ const I18N = { paneToDiagram:"Passer au diagramme", ghostTooltip:"À partir de la taille M, un élément devrait être décomposé davantage.", jumpHint:"Alt+clic : aller à la ligne dans le texte", + jumpHintTouch:"Appui long : aller à la ligne dans le texte", + tipClose:"Fermer", + tipOpenLink:"Ouvrir le lien", padReadonly:"Modifié dans le pad — lecture seule ici.", padEdit:"Ouvrir le pad pour modifier", padRefresh:"Recharger depuis le pad", @@ -1880,6 +1997,9 @@ const I18N = { paneToDiagram:"Przełącz na diagram", ghostTooltip:"Od rozmiaru M element powinien być dalej podzielony.", jumpHint:"Alt+kliknięcie: przejdź do wiersza w tekście", + jumpHintTouch:"Długie przytrzymanie: przejdź do wiersza w tekście", + tipClose:"Zamknij", + tipOpenLink:"Otwórz link", padReadonly:"Edytowane w padzie — tu tylko do czytania.", padEdit:"Otwórz pad do edycji", padRefresh:"Wczytaj ponownie z padu", @@ -1962,6 +2082,9 @@ const I18N = { paneToDiagram:"Перейти к диаграмме", ghostTooltip:"Начиная с размера M элемент следует далее декомпозировать.", jumpHint:"Alt+клик: перейти к строке в тексте", + jumpHintTouch:"Долгое нажатие: перейти к строке в тексте", + tipClose:"Закрыть", + tipOpenLink:"Открыть ссылку", padReadonly:"Редактируется в паде — здесь только чтение.", padEdit:"Открыть пад для редактирования", padRefresh:"Обновить из пада", @@ -2044,6 +2167,9 @@ const I18N = { paneToDiagram:"आरेख पर जाएँ", ghostTooltip:"आकार M से ऊपर किसी तत्व को और अधिक उप-विभाजित करना चाहिए।", jumpHint:"Alt+क्लिक: टेक्स्ट में उस पंक्ति पर जाएँ", + jumpHintTouch:"देर तक दबाएँ: टेक्स्ट में उस पंक्ति पर जाएँ", + tipClose:"बंद करें", + tipOpenLink:"लिंक खोलें", padReadonly:"पैड में संपादित होता है — यहाँ केवल पढ़ें।", padEdit:"संपादित करने के लिए पैड खोलें", padRefresh:"पैड से फिर लोड करें", @@ -2133,6 +2259,9 @@ const I18N = { implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计", ghostTooltip:"从 M 号起,元素应进一步细分。", jumpHint:"Alt+点击:跳转到文本中的该行", + jumpHintTouch:"长按:跳转到文本中的该行", + tipClose:"关闭", + tipOpenLink:"打开链接", padReadonly:"在 Pad 中编辑 — 此处只读。", padEdit:"打开 Pad 进行编辑", padRefresh:"从 Pad 重新加载", @@ -2215,6 +2344,9 @@ const I18N = { implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う", ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。", jumpHint:"Alt+クリック:テキストの該当行へ移動", + jumpHintTouch:"長押し:テキストの該当行へ移動", + tipClose:"閉じる", + tipOpenLink:"リンクを開く", padReadonly:"パッドで編集します — ここでは読み取り専用です。", padEdit:"編集するにはパッドを開く", padRefresh:"パッドから再読み込み", diff --git a/frontend/src/render.js b/frontend/src/render.js index 1120ec3..fd8cb16 100644 --- a/frontend/src/render.js +++ b/frontend/src/render.js @@ -49,8 +49,11 @@ export function esc(s){ function attr(s){ return esc(String(s)).replace(/"/g,'"'); } /* Trennstrich im Knoten-Tooltip zwischen Beschreibung und Kurz-Fakten (D40). Box-Drawing-Zeichen statt Bindestrichen: `─` stößt gapless aneinander und - liest sich als Linie, `---` als Text. */ -const TIP_RULE = '─'.repeat(24); + liest sich als Linie, `---` als Text. Exportiert, weil das Touch-Fenster + (D52) den `title` daran wieder in seine zwei Teile zerlegt — dort GIBT es + Markup, der Strich wird zur echten Linie. Eine zweite Kopie der + Beschreibung als data-Attribut wäre der einzige andere Weg gewesen. */ +export const TIP_RULE = '─'.repeat(24); /* Barrierefreier Name eines Knotens: Label + Status + Aufwand + Zuständige + Link. Die visuellen Badges (Größe, Tags, ↗) sind aria-hidden — ihre diff --git a/frontend/src/style.css b/frontend/src/style.css index 093e707..1591e33 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -815,6 +815,64 @@ /* Stationspunkt: groß, aber blass gefüllt — der Knotentext bleibt lesbar */ .cheap-dot{fill:var(--or);fill-opacity:.2;stroke:var(--or);stroke-opacity:.35;stroke-width:1.5} + /* ---------- Knoten-Fenster für Touch (SPEC §6/§9, D52) ---------- + Auf einem Zeigergerät zeigt der `title` den Tooltip; ohne Zeiger gibt es + keinen. Der einfache Tipp öffnet deshalb dieses Fenster mit demselben + Inhalt — und mit dem, was ein `title` nicht kann: Absatz, echte Trennlinie, + Knöpfe. `position:fixed` (JS setzt left/top aus `getBoundingClientRect()`, + die den `zoom` von #out schon enthält): Ein Kind von #out erbte dessen + `zoom` und würde von dessen `overflow` beschnitten (D50). */ + .nodetip{ + position:fixed;z-index:60;max-width:min(21rem, calc(100vw - 24px)); + background:var(--card);color:var(--ink); + border:1px solid rgba(36,52,71,.15);border-radius:10px; + box-shadow:0 8px 24px rgba(36,52,71,.3); + padding:10px 30px 10px 12px;font-size:.78rem;line-height:1.45; + } + .nodetip[hidden]{display:none} + .nodetip-x{ + position:absolute;top:2px;right:2px;width:26px;height:26px; + display:flex;align-items:center;justify-content:center; + background:none;border:0;border-radius:6px;cursor:pointer; + color:var(--muted);font-size:1.05rem;line-height:1; + } + .nodetip-x:hover{color:var(--ink);background:rgba(36,52,71,.06)} + /* Absätze kommen aus Leerzeilen der Notation (§1); JS baut sie zu

. */ + .nodetip-desc p + p{margin-top:.55em} + /* Die echte Linie an der Stelle, an der der Tooltip 24 `─` zeichnen musste. */ + .nodetip-facts{ + margin-top:8px;padding-top:7px;border-top:1px solid rgba(36,52,71,.15); + color:var(--muted);font-size:.72rem; + } + /* Ohne Beschreibung gibt es nichts zu trennen — der Strich stünde sonst als + erste Zeile im Fenster. Dieselbe Regel wie im Tooltip (D40-Nachtrag). */ + .nodetip-facts:first-child{margin-top:0;padding-top:0;border-top:0} + .nodetip-link{ + display:inline-flex;align-items:center;gap:5px;margin-top:9px; + padding:5px 10px;border:1px solid var(--or);border-radius:7px; + color:var(--or);text-decoration:none;font-size:.74rem;font-weight:500; + } + /* Die Spitze zeigt auf den Knoten — ohne sie wäre bei dicht stehenden Knoten + nicht zu sehen, welcher gemeint ist. Ein gedrehtes Quadrat mit zwei Kanten: + so erbt es Rahmen UND Schatten des Fensters. JS setzt --tipx (Spitze) und + dreht sie per .above nach unten, wenn das Fenster über dem Knoten sitzt. */ + .nodetip-arrow{ + position:absolute;top:-6px;left:var(--tipx, 50%);width:11px;height:11px; + margin-left:-5.5px;background:var(--card); + border-left:1px solid rgba(36,52,71,.15); + border-top:1px solid rgba(36,52,71,.15); + transform:rotate(45deg); + } + .nodetip.above .nodetip-arrow{ + top:auto;bottom:-6px; + border:0;border-right:1px solid rgba(36,52,71,.15); + border-bottom:1px solid rgba(36,52,71,.15); + } + /* Solange das Fenster offen ist, trägt sein Knoten einen Petrol-Ring — wie + der „scharfe" Knoten beim langen Druck (D25). #out-Präfix nötig, weil + `ul.or .node{box-shadow:none}` spezifischer ist. */ + #out .node.tipped{box-shadow:0 0 0 3px rgba(15,118,110,.55)} + /* --- ALL OF: Kinder nebeneinander, durchgezogener Verteiler --- */ ul.and{display:flex;align-items:flex-start;position:relative;padding-top:14px} ul.and::before{ @@ -1210,7 +1268,7 @@ @media print{ html,body{height:auto!important;overflow:visible!important;background:#fff!important} header,.panel.editor,.gutter,.panel.right .panel-head, - .warnings,.zoomctl,.site-footer{display:none!important} + .warnings,.zoomctl,.site-footer,.nodetip{display:none!important} .app,.app.side{display:block!important;max-width:none!important;width:auto!important; height:auto!important;margin:0!important;gap:0!important} .panel.right{grid-area:auto!important;background:#fff!important;border:0!important; @@ -1228,6 +1286,7 @@ .node.current{transform:none!important;animation:none!important} .node.current::after{display:none!important} .node.fresh{box-shadow:none!important} /* „neu seit dem letzten Besuch" ist persönlich (D28) */ + .node.tipped{box-shadow:none!important} /* offenes Knoten-Fenster ist Bedienung (D52) */ /* Faltung (D38): das ▾ offener Knoten ist Bedienelement — nicht drucken; das „▸ n" eingeklappter Knoten ist Kennzeichnung und bleibt. */ .node:not(.folded) .fold{display:none!important}