From 0ec29e96e8c24d7d47ad140fefe924c4bd889ac2 Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Tue, 1 Sep 2026 01:30:25 +0200 Subject: [PATCH] fix(state): draw the note edge dashed under the neo look The edge joining a state to its note rendered as a solid line under `look: neo`. Reproduces on `develop` today with any theme -- it is a neo bug, not a theme one -- but making neo the default is what turns a long-standing quirk into everyone's problem. The dashes were expressed only as CSS: `.note-edge { stroke-dasharray: 5 }` in `state/styles.js`. That is enough under `classic`, which writes no inline stroke properties. Under `neo`, `insertEdge` writes an *inline* `stroke-dasharray` on every edge, computed from the path length -- for a solid edge, one long run trimmed at both ends so the arrow markers keep their gaps. An inline style outranks a stylesheet rule, so the note edge took the solid pattern with `.note-edge` still present and simply losing. `insertEdge` picks between its two dash generators on `edge.pattern`, so that is where the dashing belongs. The note edge now declares `pattern: 'dashed'`, which routes it through the same generator every other dashed edge uses and so keeps the marker gaps. The `note-edge` class stays: `classic` has no inline dasharray, so that rule is what dashes the line there, and it beats `edge-pattern-dashed` on source order at equal specificity. Dropping it would change classic's dash length from 5 to 3. Verified by rendering: under neo the note edge goes from a single 93px run to 49 segments of 2px, while `Active --> Idle` stays solid at one run; under classic both are unchanged. Three unit tests pin the pattern, that transitions stay patternless, and that the class is retained; removing the fix fails the first. `stateDiagram-neo.spec.js` already renders a state with a note, so Argos covers this visually. Unit suite 5941 passing (2 pre-existing domus harness env-var failures); lint, Prettier, cspell and build:types clean. --- .changeset/state-note-edge-neo-dashes.md | 7 ++ .../mermaid/src/diagrams/state/dataFetcher.ts | 12 ++++ .../mermaid/src/diagrams/state/stateDb.ts | 5 ++ .../src/diagrams/state/stateNoteEdge.spec.ts | 66 +++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 .changeset/state-note-edge-neo-dashes.md create mode 100644 packages/mermaid/src/diagrams/state/stateNoteEdge.spec.ts diff --git a/.changeset/state-note-edge-neo-dashes.md b/.changeset/state-note-edge-neo-dashes.md new file mode 100644 index 00000000000..9e792e59b78 --- /dev/null +++ b/.changeset/state-note-edge-neo-dashes.md @@ -0,0 +1,7 @@ +--- +'mermaid': patch +--- + +fix(state): draw the edge between a state and its note dashed under the `neo` look. It was rendering solid. + +The dashes were expressed only as CSS — `.note-edge { stroke-dasharray: 5 }` — which is enough under `classic` but not under `neo`, where `insertEdge` writes an inline `stroke-dasharray` on every edge, computed from the path length so the arrow markers keep their gaps. An inline style outranks a stylesheet rule, so the note edge took the solid pattern and the `.note-edge` rule simply lost. The note edge now declares `pattern: 'dashed'`, which is what `insertEdge` reads to choose its dash generator. `classic` is unchanged. diff --git a/packages/mermaid/src/diagrams/state/dataFetcher.ts b/packages/mermaid/src/diagrams/state/dataFetcher.ts index 76212656207..8b4bf27a358 100644 --- a/packages/mermaid/src/diagrams/state/dataFetcher.ts +++ b/packages/mermaid/src/diagrams/state/dataFetcher.ts @@ -373,6 +373,18 @@ export const dataFetcher = ( style: G_EDGE_STYLE, labelStyle: '', classes: CSS_EDGE_NOTE_EDGE, + // The dashes have to be declared on the edge, not only through the `note-edge` + // class. Under `look: neo`, `insertEdge` writes an *inline* `stroke-dasharray` + // computed from the path length -- a solid run trimmed at both ends so the arrow + // markers get their gaps -- and it picks that pattern from `edge.pattern`. An + // inline style outranks the stylesheet, so a note edge that only carried the class + // was drawn solid: the `.note-edge` rule was still there and simply lost. + // + // Naming the pattern here routes it through the same dash generator every other + // dashed edge uses, so the marker gaps survive. `classic` is untouched: it writes + // no inline dasharray, and `.note-edge` still wins over `edge-pattern-dashed` + // because it is emitted later in the sheet at equal specificity. + pattern: 'dashed', arrowheadStyle: G_EDGE_ARROWHEADSTYLE, labelpos: G_EDGE_LABELPOS, labelType: G_EDGE_LABELTYPE, diff --git a/packages/mermaid/src/diagrams/state/stateDb.ts b/packages/mermaid/src/diagrams/state/stateDb.ts index b39d1f81d61..8c3953ca61b 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.ts +++ b/packages/mermaid/src/diagrams/state/stateDb.ts @@ -182,6 +182,11 @@ export interface Edge { thickness: string; classes: string; look: MermaidConfig['look']; + /** + * Stroke pattern, read by `insertEdge`. The note edge is the only state edge that sets + * it; see the note-edge push in `dataFetcher.ts` for why the CSS class is not enough. + */ + pattern?: 'solid' | 'dotted' | 'dashed'; } /** diff --git a/packages/mermaid/src/diagrams/state/stateNoteEdge.spec.ts b/packages/mermaid/src/diagrams/state/stateNoteEdge.spec.ts new file mode 100644 index 00000000000..a08edfc4c01 --- /dev/null +++ b/packages/mermaid/src/diagrams/state/stateNoteEdge.spec.ts @@ -0,0 +1,66 @@ +/** + * The edge joining a state to its note is dashed. That used to be expressed only as CSS -- + * `.note-edge { stroke-dasharray: 5 }` in `state/styles.js` -- which is enough under the + * `classic` look and not enough under `neo`. + * + * Under `neo`, `insertEdge` writes an *inline* `stroke-dasharray` on every edge, computed + * from the path length: for a solid edge, one long run trimmed at both ends so the arrow + * markers have their gaps. An inline style outranks a stylesheet rule, so the note edge + * came out solid with the `.note-edge` rule still present and simply losing. `neo` became + * the default look, which is what made a long-standing quirk everyone's problem. + * + * `insertEdge` chooses between the two dash generators on `edge.pattern`, so that is where + * the dashing has to be declared. This pins it there. + */ +// @ts-expect-error No types available for JISON +import stateDiagram, { parser } from './parser/stateDiagram.jison'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { StateDB } from './stateDb.js'; + +describe('state note edges', () => { + let stateDb: StateDB; + + beforeEach(() => { + stateDb = new StateDB(2); + parser.yy = stateDb; + stateDiagram.parser.yy = stateDb; + stateDiagram.parser.yy.clear(); + }); + + const edges = (diagram: string) => { + parser.parse(diagram); + return stateDb.getData().edges; + }; + + const diagram = `stateDiagram-v2 + [*] --> Active + Active --> Idle + note right of Active + A note + end note + `; + + it('declares the note edge as dashed on the edge, not only in CSS', () => { + const noteEdge = edges(diagram).find((edge) => edge.classes.includes('note-edge')); + expect(noteEdge).toBeDefined(); + // `insertEdge` reads this to pick its dash generator under the neo look. Without it the + // edge takes the solid generator and the inline result hides `.note-edge`. + expect(noteEdge?.pattern).toBe('dashed'); + }); + + it('leaves ordinary transitions solid', () => { + // The fix must not dash every edge: `pattern` is absent on transitions, which is what + // sends them down the solid branch. + const transitions = edges(diagram).filter((edge) => !edge.classes.includes('note-edge')); + expect(transitions.length).toBeGreaterThan(0); + expect(transitions.every((edge) => edge.pattern === undefined)).toBe(true); + }); + + it('keeps the note-edge class, which is what the classic look still styles', () => { + // `classic` writes no inline dasharray, so it is the `.note-edge` rule that dashes the + // line there -- and that rule beats `edge-pattern-dashed` on source order. Dropping the + // class in favour of the pattern alone would change the classic look's dash length. + const noteEdge = edges(diagram).find((edge) => edge.classes.includes('note-edge')); + expect(noteEdge?.classes).toContain('note-edge'); + }); +});