Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Add: Large [Mermaid diagrams](visuals/mermaid-diagrams) can be zoomed and panned in a full screen overlay
4 changes: 4 additions & 0 deletions znai-docs/znai/release-notes/2026.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.91

:include-markdowns: 1.91

# 1.90.1

:include-markdowns: 1.90.1
Expand Down
45 changes: 45 additions & 0 deletions znai-docs/znai/visuals/mermaid-diagrams.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,51 @@ In presentation mode, rendered expressions will automatically scale to make use

Note: Rendering is done by using [Mermaid](https://mermaid-js.github.io/mermaid/#/) library.

# Large Diagrams

A diagram that is too large to fit the page width is shrunk to fit and can become hard to read.
Click the diagram to open it in a full screen overlay where you can:

- scroll/`wheel` to zoom towards the cursor
- drag to pan

```mermaid
flowchart LR
Client[Web & Mobile Clients] --> Gateway[API Gateway]
Gateway --> Auth[Auth Service]
Gateway --> Catalog[Catalog Service]
Gateway --> Cart[Cart Service]
Gateway --> Orders[Order Service]
Gateway --> Payments[Payment Service]
Gateway --> Shipping[Shipping Service]
Gateway --> Notifications[Notification Service]

Auth --> AuthDb[(Auth DB)]
Catalog --> CatalogDb[(Catalog DB)]
Catalog --> Search[(Search Index)]
Cart --> CartCache[(Cart Cache)]
Orders --> OrdersDb[(Orders DB)]
Orders --> Queue[[Event Queue]]
Payments --> PaymentsDb[(Payments DB)]
Payments --> Gatewayp[External Payment Gateway]
Shipping --> ShippingDb[(Shipping DB)]
Shipping --> Carrier[External Carrier API]

Queue --> Notifications
Queue --> Analytics[Analytics Pipeline]
Queue --> Warehouse[(Data Warehouse)]
Analytics --> Warehouse
Notifications --> Email[Email Provider]
Notifications --> SMS[SMS Provider]
Notifications --> Push[Push Provider]

Analytics --> Dashboards[BI Dashboards]
Warehouse --> Dashboards

click Catalog "visuals/graphviz-diagrams"
click Payments href "https://mermaid.js.org" "Mermaid documentation"
```

# External File

Use include plugin to render a Mermaid diagram from a file.
Expand Down
9 changes: 6 additions & 3 deletions znai-reactjs/src/doc-elements/Documentation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,8 @@ export class Documentation extends React.Component {
);

const footnotes = isFootnoteListHidden() ? [] : pageContentProcessor.extractFootnotes(page.content);
const renderedFootnotesList = footnotes.length > 0 ? (
<FootnotesList footnotes={footnotes} elementsLibrary={elementsLibrary} />
) : null;
const renderedFootnotesList =
footnotes.length > 0 ? <FootnotesList footnotes={footnotes} elementsLibrary={elementsLibrary} /> : null;

const NextPrevNavigation = pageTypesRegistry.nextPrevNavigationComponent(page.tocItem);
const renderedNextPrevNavigation = (
Expand Down Expand Up @@ -467,6 +466,10 @@ export class Documentation extends React.Component {

this.extractPageSectionNodes();

if (this.cancelPendingScrollRestore) {
this.cancelPendingScrollRestore();
}

const currentPageLocation = documentationNavigation.currentPageLocation();

if (urlHistoryState && urlHistoryState.scrollTop) {
Expand Down
24 changes: 24 additions & 0 deletions znai-reactjs/src/doc-elements/mermaid/Mermaid.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,23 @@
* limitations under the License.
*/

.znai-mermaid {
position: relative;
}

.znai-mermaid.wide {
display: flex;
justify-content: center;
}

.znai-mermaid-zoomable {
cursor: zoom-in;
}

.znai-mermaid-zoomable:hover::after {
opacity: 0.95;
}

.znai-mermaid svg text.messageText {
stroke: none !important;
}
Expand All @@ -27,6 +39,18 @@
cursor: pointer;
}

/*
* Mermaid scopes the node text color to the diagram id (e.g. #znai-mermaid-id-3 span), so a linked
* node's label looks like any other node. Recolor it with znai's link color - !important is needed
* to win over that id-scoped rule - so links are recognizable, including in the dark theme where
* znai links carry no underline.
*/
.znai-mermaid svg .clickable .nodeLabel,
.znai-mermaid svg .clickable .nodeLabel p {
color: var(--znai-link-color) !important;
fill: var(--znai-link-color) !important;
}

.znai-mermaid svg .clickable:hover rect,
.znai-mermaid svg .clickable:hover circle,
.znai-mermaid svg .clickable:hover polygon,
Expand Down
8 changes: 8 additions & 0 deletions znai-reactjs/src/doc-elements/mermaid/Mermaid.demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@
import React from "react";

import Mermaid from "./Mermaid";
import { ZoomOverlay } from "../zoom/ZoomOverlay";
import { Registry } from "react-component-viewer";
import c4DiagramContent from './c4context.txt?raw'; // Add this import
import awsDiagramContent from './aws-diagram.txt?raw';
import largeDiagramContent from './large-diagram.txt?raw';

export function mermaidDemo(registry: Registry) {
registry.add("simple", () => <Mermaid mermaid={"graph TD; A-->B; B-->C;"} />);
registry.add("large (zoom & pan)", () => (
<>
<ZoomOverlay />
<Mermaid mermaid={largeDiagramContent} />
</>
));
registry.add("with links", () => (
<Mermaid
mermaid={
Expand Down
110 changes: 42 additions & 68 deletions znai-reactjs/src/doc-elements/mermaid/Mermaid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,85 +14,50 @@
* limitations under the License.
*/

import React, { useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";

import mermaid from "mermaid";
import { isLocalUrl } from "../../structure/links";
import { documentationNavigation } from "../../structure/DocumentationNavigation";
import { isZnaiDarkTheme, useZnaiThemeChange } from "../../theme/znaiTheme";
import { zoom } from "../zoom/Zoom";
import { useMermaidSvg, MermaidIconPack } from "./useMermaidSvg";
import { isMermaidSvgShrunk } from "./mermaidSvgSize";
import { MermaidZoomed } from "./MermaidZoomed";

import "./Mermaid.css";

interface Props {
mermaid: string;
wide?: boolean;
iconpacks?: Array<{ name: string; url: string }>;
iconpacks?: MermaidIconPack[];
}

let mermaidIdIdx = 0;
function generateNewMermaidId() {
return "znai-mermaid-id-" + mermaidIdIdx++;
}

let isMermaidInitialized = false;
function initMermaidIfRequired() {
if (isMermaidInitialized) {
return;
}

window.znaiTheme.addChangeHandler(initializeMermaid);
export default function Mermaid(props: Props) {
const html = useMermaidSvg(props.mermaid, props.iconpacks);
const containerRef = useRef<HTMLDivElement>(null);

initializeMermaid();
isMermaidInitialized = true;
// a large diagram is shrunk to fit the page width and becomes hard to read; in that case we let
// the reader open it in a full screen zoom & pan overlay. Re-checked on resize as the available
// width (and thus whether the diagram fits) changes with the window/layout.
const [isZoomable, setIsZoomable] = useState(false);

function initializeMermaid() {
mermaid.initialize({
startOnLoad: false,
securityLevel: "strict",
theme: mermaidThemeName(),
});
}
}
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}

export default function Mermaid(props: Props) {
const [html, setHTML] = React.useState("");
const [znaiThemeName, setZnaiThemeName] = useState(() => window.znaiTheme.name);

useZnaiThemeChange((name) => {
setZnaiThemeName(name);
});

React.useEffect(() => {
initMermaidIfRequired();
}, []);

React.useEffect(() => {
const id = generateNewMermaidId();
mermaid.initialize({
startOnLoad: false,
securityLevel: "strict",
theme: mermaidThemeName(),
});
// Register icon packs if provided, otherwise use default logos pack
if (props.iconpacks && props.iconpacks.length > 0) {
mermaid.registerIconPacks(props.iconpacks.map(pack => ({
name: pack.name,
loader: () => fetch(pack.url).then(res => res.json())
})));
function update() {
setIsZoomable(isMermaidSvgShrunk(containerRef.current));
}

mermaid.render(id, props.mermaid)
.then(({ svg }) => {
setHTML(svg);
})
.catch((error) => {
console.error('Error rendering mermaid diagram:', error);
});
}, [props.mermaid, props.iconpacks, znaiThemeName]);
update();

const containerRef = useRef<HTMLDivElement>(null);
const observer = new ResizeObserver(update);
observer.observe(container);
return () => observer.disconnect();
}, [html]);

React.useEffect(() => {
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
Expand All @@ -114,12 +79,21 @@ export default function Mermaid(props: Props) {

container.addEventListener("click", handleClick);
return () => container.removeEventListener("click", handleClick);
}, [html]);
}, [html, isZoomable]);

const className = "znai-mermaid " + (props.wide ? "wide" : "content-block");
return <div ref={containerRef} className={className} dangerouslySetInnerHTML={{ __html: html }}></div>;
}
function openZoom() {
zoom.zoom(<MermaidZoomed mermaid={props.mermaid} iconpacks={props.iconpacks} />);
}

function mermaidThemeName() {
return isZnaiDarkTheme() ? "dark" : "default";
}
const className =
"znai-mermaid " + (props.wide ? "wide" : "content-block") + (isZoomable ? " znai-mermaid-zoomable" : "");

return (
<div
ref={containerRef}
className={className}
onClick={isZoomable ? openZoom : undefined}
dangerouslySetInnerHTML={{ __html: html }}
></div>
);
}
79 changes: 79 additions & 0 deletions znai-reactjs/src/doc-elements/mermaid/MermaidZoomed.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2026 znai maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

.znai-mermaid.zoomed {
width: 92vw;
height: 92vh;
background-color: var(--znai-background-color);
overflow: hidden;
}

.znai-mermaid-zoomed-surface {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
touch-action: none;
user-select: none;
}

.znai-mermaid-zoomed-content {
position: absolute;
top: 0;
left: 0;
will-change: transform;
}

.znai-mermaid-zoomed-svg svg {
display: block;
}

.znai-mermaid-zoomed-toolbar {
position: absolute;
top: 12px;
right: 12px;
z-index: 2;

display: flex;
align-items: center;
gap: 10px;

padding: 6px 12px;
border-radius: 6px;
border: var(--znai-zoom-container-border);
background-color: var(--znai-background-color);
}

.znai-mermaid-zoomed-button {
cursor: pointer;
opacity: 0.55;
width: 20px;
height: 20px;
}

.znai-mermaid-zoomed-button:hover {
opacity: 1;
}

.znai-mermaid-zoomed-percent {
min-width: 42px;
text-align: center;
font-size: var(--znai-small-meta-text-size);
color: var(--znai-regular-text-light-color);
user-select: none;
}
Loading
Loading