fix(diagram): Ticket-Refs im Label brechen nicht mehr am Bindestrich um (SPEC §9)

Der Browser bricht eine zu breit geratene Zeile an jedem Bindestrich —
aus #US-123 wurde #US-/123 über zwei Zeilen. Eine tref-Spanne mit
white-space:nowrap hält das Token zusammen; labelLines() (Export) misst
ihre Textknoten unverändert mit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-27 17:14:27 +02:00
co-authored by Claude Fable 5
parent 3f336db680
commit 83eb6ff77f
5 changed files with 44 additions and 2 deletions
+1
View File
@@ -19,6 +19,7 @@ reverse.
## 2026-08-27 ## 2026-08-27
- A ticket ref like `#US-123` in a node title no longer breaks across two lines at its hyphen
- Create a Taiga story from a diagram node: the node window offers "create story" and "create story + tasks" — log in once (the token stays in the browser), pick the project, and the ref lands in the line as `#US-123`, undoable - Create a Taiga story from a diagram node: the node window offers "create story" and "create story + tasks" — log in once (the token stays in the browser), pick the project, and the ref lands in the line as `#US-123`, undoable
- Sub-packages become Taiga tasks via a checkbox dialog: required and optional children start checked, in `|`/`=` groups only realized alternatives, done children unchecked, discarded ones absent — each created task writes its `#T-…` ref to its line - Sub-packages become Taiga tasks via a checkbox dialog: required and optional children start checked, in `|`/`=` groups only realized alternatives, done children unchecked, discarded ones absent — each created task writes its `#T-…` ref to its line
- The project dialog is prefilled from the inherited `&taiga.slug` keyword, and a creation without or against the inherited assignment writes the keyword back to the node's line - The project dialog is prefilled from the inherited `&taiga.slug` keyword, and a creation without or against the inherited assignment writes the keyword back to the node's line
+3 -1
View File
@@ -565,7 +565,9 @@ Siehe D29.
trifft der 23-px-Abzweig die Mitte der **ersten Zeile** (die Zeilenhöhe trifft der 23-px-Abzweig die Mitte der **ersten Zeile** (die Zeilenhöhe
bleibt fest); der Optional-Kreis (§9) sitzt auf dem Abzweig, nicht auf der bleibt fest); der Optional-Kreis (§9) sitzt auf dem Abzweig, nicht auf der
Knotenmitte. Der Grafikexport misst die gerenderten Zeilen und gibt sie als Knotenmitte. Der Grafikexport misst die gerenderten Zeilen und gibt sie als
einzelne Textzeilen aus. einzelne Textzeilen aus. Eine **Ticket-Referenz** im Label (`#US-123` /
`#T-1234`, §11) bricht dabei **nie mitten im Token** um — auch nicht am
Bindestrich, an dem der Browser eine zu breit geratene Zeile sonst bricht.
- Dasselbe gilt **waagerecht**: Im horizontalen Fächer trifft der Stiel die - Dasselbe gilt **waagerecht**: Im horizontalen Fächer trifft der Stiel die
**Knotenmitte**, nicht die Mitte der Zelle. Beides fällt nur zusammen, solange **Knotenmitte**, nicht die Mitte der Zelle. Beides fällt nur zusammen, solange
der Knoten in seiner Zelle zentriert steht — ein Knoten mit any-of-Kindern der Knoten in seiner Zelle zentriert steht — ein Knoten mit any-of-Kindern
+13 -1
View File
@@ -93,6 +93,18 @@ export function wrapLabel(label, max = 32){
return lines; return lines;
} }
/* Ticket-Referenzen (`#US-123`/`#T-1234`, SPEC §11) im Label bleiben ein
Stück: `wrapLabel()` bricht nur an Leerzeichen, aber der Browser bricht
eine Zeile, die breiter als `max-width` gerät (32ch misst die Ziffer „0",
Buchstaben sind breiter), zusätzlich an jedem BINDESTRICH — und eine Ref,
die als `#US-`/`123` über zwei Zeilen geht, ist nicht mehr als Ref zu
lesen. Läuft auf dem schon escapten Label-Text; die Spanne enthält nur
Text, `labelLines()` (Grafikexport) misst ihre Textknoten unverändert mit. */
function markTicketRefs(html){
return html.replace(/(^|\s)(#(?:US|T)-\d+)(?=\s|$)/g,
(all, pre, ref) => `${pre}<span class="tref">${ref}</span>`);
}
/* Barrierefreier Name eines Knotens: Label + Status + Aufwand + Zuständige + /* Barrierefreier Name eines Knotens: Label + Status + Aufwand + Zuständige +
Link. Die visuellen Badges (Größe, Tags, ↗) sind aria-hidden — ihre Link. Die visuellen Badges (Größe, Tags, ↗) sind aria-hidden — ihre
Information steckt hier, sonst würde der Screenreader Kryptisches („M", Information steckt hier, sonst würde der Screenreader Kryptisches („M",
@@ -272,7 +284,7 @@ function nodeHtml(n, extra, opts, fold){
Der `textContent` behielte sonst keine Wortgrenze, und alles, Der `textContent` behielte sonst keine Wortgrenze, und alles,
was den Knotentext liest (Export, Fokusmarken-Schlüssel), was den Knotentext liest (Export, Fokusmarken-Schlüssel),
bekäme zusammengeklebte Wörter. */ bekäme zusammengeklebte Wörter. */
esc(wrapLabel(n.label).join('\n')) + markTicketRefs(esc(wrapLabel(n.label).join('\n'))) +
/* ”-Marke (D40): macht die sonst unsichtbare Beschreibung /* ”-Marke (D40): macht die sonst unsichtbare Beschreibung
auffindbar (Lehre aus D25) — spiegelt das "-Zeichen der auffindbar (Lehre aus D25) — spiegelt das "-Zeichen der
Notation. Nicht im Export: Der Text selbst kann dort nicht Notation. Nicht im Export: Der Text selbst kann dort nicht
+4
View File
@@ -775,6 +775,10 @@
} }
/* Nur die dunkle (statuslose) Wurzel — Begründung beim Falt-Chip oben. */ /* Nur die dunkle (statuslose) Wurzel — Begründung beim Falt-Chip oben. */
.root-node:not(.st-idee):not(.st-geplant):not(.st-arbeit):not(.st-durchstich):not(.st-fertig):not(.st-prod):not(.st-highrisk):not(.st-verworfen) .nid{color:rgba(255,255,255,.75)} .root-node:not(.st-idee):not(.st-geplant):not(.st-arbeit):not(.st-durchstich):not(.st-fertig):not(.st-prod):not(.st-highrisk):not(.st-verworfen) .nid{color:rgba(255,255,255,.75)}
/* Ticket-Referenz im Label (SPEC §11): als Ganzes gehalten — der Browser
bräche eine zu breit geratene Zeile sonst am Bindestrich mitten im Token
(`#US-`/`123`). Die Spanne setzt render.js (markTicketRefs). */
.node .tref{white-space:nowrap}
/* Der Knopf trägt das Zeichen selbst statt eines Icons — in der Größe der /* Der Knopf trägt das Zeichen selbst statt eines Icons — in der Größe der
SVGs daneben, damit die Kopfzeile ihre Höhe behält. */ SVGs daneben, damit die Kopfzeile ihre Höhe behält. */
.copybtn.idsbtn span{ .copybtn.idsbtn span{
+23
View File
@@ -182,3 +182,26 @@ describe('showIds — die ID in einer eigenen Zeile über dem Titel', () => {
expect(html).toContain('&lt;b: X'); expect(html).toContain('&lt;b: X');
}); });
}); });
/* Ticket-Referenzen im Label (SPEC §11) sind als Ganzes gehalten: eine
`tref`-Spanne mit `white-space:nowrap`, damit der Browser eine zu breit
geratene Zeile nicht am Bindestrich mitten im Token bricht. */
describe('renderTreeHtml — Ticket-Referenzen brechen nicht um', () => {
const one = text => renderTreeHtml(parse(text).roots, {t, cheapSet: new Set()}).html;
it('umspannt eine freistehende Ref im Label', () => {
expect(one('- Login bauen #auth #US-123'))
.toContain('<span class="tref">#US-123</span>');
expect(one('- #x: Detail #T-4567 klären'))
.toContain('<span class="tref">#T-4567</span>');
});
it('umspannt auch den D60-Knoten, dessen Label die Ref ist', () => {
expect(one('- #US-123')).toContain('<span class="tref">#US-123</span>');
});
it('lässt Zitier-Klammern und Nicht-Refs in Ruhe', () => {
expect(one('- Erwähnt (#US-123) nur #x')).not.toContain('tref');
expect(one('- #auth: Backend-US-123 Modul')).not.toContain('tref');
});
});