@keboola/design 9.1.0 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/AGENTS.md +117 -40
  2. package/COMPONENT-API.md +31 -9
  3. package/README.md +3 -3
  4. package/dist/MermaidDiagram-EOKJHQQV.js +10 -0
  5. package/dist/MermaidDiagram-EOKJHQQV.js.map +1 -0
  6. package/dist/charts.d.ts +31 -2
  7. package/dist/charts.js +6 -9
  8. package/dist/charts.js.map +1 -1
  9. package/dist/chunk-63NSN2VL.js +274 -0
  10. package/dist/chunk-63NSN2VL.js.map +1 -0
  11. package/dist/{rjsf.cjs → chunk-6FPEI4EM.js} +1491 -5701
  12. package/dist/chunk-6FPEI4EM.js.map +1 -0
  13. package/dist/chunk-7PUUHEJQ.js +1313 -0
  14. package/dist/chunk-7PUUHEJQ.js.map +1 -0
  15. package/dist/chunk-BOHNXTMM.js +606 -0
  16. package/dist/chunk-BOHNXTMM.js.map +1 -0
  17. package/dist/{testing.cjs → chunk-C3L3TBBT.js} +33 -164
  18. package/dist/chunk-C3L3TBBT.js.map +1 -0
  19. package/dist/chunk-CTCFYWJC.js +407 -0
  20. package/dist/chunk-CTCFYWJC.js.map +1 -0
  21. package/dist/chunk-ESBL3IPF.js +23 -0
  22. package/dist/chunk-ESBL3IPF.js.map +1 -0
  23. package/dist/chunk-IJTHFNNJ.js +261 -0
  24. package/dist/chunk-IJTHFNNJ.js.map +1 -0
  25. package/dist/chunk-JP7PNHLH.js +40 -0
  26. package/dist/chunk-JP7PNHLH.js.map +1 -0
  27. package/dist/chunk-KPNAW5MU.js +229 -0
  28. package/dist/chunk-KPNAW5MU.js.map +1 -0
  29. package/dist/chunk-L37JJ6MR.js +152 -0
  30. package/dist/chunk-L37JJ6MR.js.map +1 -0
  31. package/dist/chunk-Q7NWXLCT.js +90 -0
  32. package/dist/chunk-Q7NWXLCT.js.map +1 -0
  33. package/dist/chunk-SYIEF644.js +1016 -0
  34. package/dist/chunk-SYIEF644.js.map +1 -0
  35. package/dist/chunk-VMFSRV5H.js +16 -0
  36. package/dist/chunk-VMFSRV5H.js.map +1 -0
  37. package/dist/chunk-VPPM7UUQ.js +47 -0
  38. package/dist/chunk-VPPM7UUQ.js.map +1 -0
  39. package/dist/chunk-XV3UUOYF.js +845 -0
  40. package/dist/chunk-XV3UUOYF.js.map +1 -0
  41. package/dist/editors.d.ts +27 -3
  42. package/dist/editors.js +85 -2081
  43. package/dist/editors.js.map +1 -1
  44. package/dist/icons.d.ts +29 -8
  45. package/dist/icons.js +2 -361
  46. package/dist/icons.js.map +1 -1
  47. package/dist/index.d.ts +709 -425
  48. package/dist/index.js +5042 -13885
  49. package/dist/index.js.map +1 -1
  50. package/dist/markdown-editor.js +41 -3426
  51. package/dist/markdown-editor.js.map +1 -1
  52. package/dist/mermaid.js +8 -1129
  53. package/dist/mermaid.js.map +1 -1
  54. package/dist/rjsf.js +180 -9381
  55. package/dist/rjsf.js.map +1 -1
  56. package/dist/styles.css +1 -1
  57. package/dist/testing.js +5 -357
  58. package/dist/testing.js.map +1 -1
  59. package/package.json +28 -73
  60. package/src/styles.css +60 -3
  61. package/src/theme.css +1 -0
  62. package/dist/charts.cjs +0 -268
  63. package/dist/charts.cjs.map +0 -1
  64. package/dist/charts.d.cts +0 -181
  65. package/dist/constants-D3gaMY-W.d.cts +0 -39
  66. package/dist/editors.cjs +0 -2440
  67. package/dist/editors.cjs.map +0 -1
  68. package/dist/editors.d.cts +0 -165
  69. package/dist/icons.cjs +0 -23888
  70. package/dist/icons.cjs.map +0 -1
  71. package/dist/icons.d.cts +0 -20
  72. package/dist/index.cjs +0 -24894
  73. package/dist/index.cjs.map +0 -1
  74. package/dist/index.d.cts +0 -10438
  75. package/dist/markdown-editor.cjs +0 -4636
  76. package/dist/markdown-editor.cjs.map +0 -1
  77. package/dist/markdown-editor.d.cts +0 -75
  78. package/dist/mermaid.cjs +0 -1138
  79. package/dist/mermaid.cjs.map +0 -1
  80. package/dist/mermaid.d.cts +0 -15
  81. package/dist/rjsf.cjs.map +0 -1
  82. package/dist/rjsf.d.cts +0 -55
  83. package/dist/testing.cjs.map +0 -1
  84. package/dist/testing.d.cts +0 -25
@@ -0,0 +1,261 @@
1
+ import { Skeleton } from './chunk-VPPM7UUQ.js';
2
+ import { ClipboardButton } from './chunk-L37JJ6MR.js';
3
+ import { Button } from './chunk-CTCFYWJC.js';
4
+ import { Minimize2, Maximize2 } from './chunk-BOHNXTMM.js';
5
+ import { Tooltip } from './chunk-63NSN2VL.js';
6
+ import { cn } from './chunk-JP7PNHLH.js';
7
+ import { jsx, jsxs } from 'react/jsx-runtime';
8
+ import { useMemo, useRef, useState, useEffect, useCallback } from 'react';
9
+ import { createPortal } from 'react-dom';
10
+ import DOMPurify from 'dompurify';
11
+
12
+ var DiagramSkeleton = () => /* @__PURE__ */ jsx("div", { className: "tw:my-3 tw:overflow-hidden tw:rounded-md tw:border tw:border-neutral-150 tw:bg-neutral-50 tw:p-6", children: /* @__PURE__ */ jsx(Skeleton, { className: "tw:h-40" }) });
13
+ var MIN_SCALE = 0.5;
14
+ var MAX_SCALE = 3;
15
+ var ZOOM_STEP = 0.1;
16
+ var ZoomableSvg = ({
17
+ html,
18
+ copyText,
19
+ className,
20
+ allowForeignObject,
21
+ renderActions
22
+ }) => {
23
+ const safeHtml = useMemo(
24
+ () => DOMPurify.sanitize(html, {
25
+ USE_PROFILES: { svg: true, svgFilters: true },
26
+ ...allowForeignObject ? { ADD_TAGS: ["foreignObject"] } : {}
27
+ }),
28
+ [html, allowForeignObject]
29
+ );
30
+ const containerRef = useRef(null);
31
+ const [scale, setScale] = useState(1);
32
+ const [translate, setTranslate] = useState({ x: 0, y: 0 });
33
+ const [isFullscreen, setIsFullscreen] = useState(false);
34
+ useEffect(() => {
35
+ if (!isFullscreen) return;
36
+ const onKeyDown = (e) => {
37
+ if (e.key === "Escape") {
38
+ e.stopPropagation();
39
+ setIsFullscreen(false);
40
+ }
41
+ };
42
+ document.addEventListener("keydown", onKeyDown, { capture: true });
43
+ return () => document.removeEventListener("keydown", onKeyDown, { capture: true });
44
+ }, [isFullscreen]);
45
+ const dragRef = useRef(null);
46
+ useEffect(() => {
47
+ const el = containerRef.current;
48
+ if (!el) return;
49
+ const onWheel = (e) => {
50
+ if (!e.ctrlKey && !e.metaKey) return;
51
+ e.preventDefault();
52
+ const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
53
+ setScale((s) => Math.min(MAX_SCALE, Math.max(MIN_SCALE, s + delta)));
54
+ };
55
+ el.addEventListener("wheel", onWheel, { passive: false });
56
+ return () => el.removeEventListener("wheel", onWheel);
57
+ }, [isFullscreen]);
58
+ const didDragRef = useRef(false);
59
+ const handleMouseDown = useCallback(
60
+ (e) => {
61
+ if (e.button !== 0) return;
62
+ e.preventDefault();
63
+ didDragRef.current = false;
64
+ dragRef.current = {
65
+ startX: e.clientX,
66
+ startY: e.clientY,
67
+ startTx: translate.x,
68
+ startTy: translate.y
69
+ };
70
+ },
71
+ [translate]
72
+ );
73
+ const handleMouseMove = useCallback((e) => {
74
+ if (!dragRef.current) return;
75
+ didDragRef.current = true;
76
+ setTranslate({
77
+ x: dragRef.current.startTx + e.clientX - dragRef.current.startX,
78
+ y: dragRef.current.startTy + e.clientY - dragRef.current.startY
79
+ });
80
+ }, []);
81
+ const handleMouseUp = useCallback(() => {
82
+ dragRef.current = null;
83
+ }, []);
84
+ const handleReset = useCallback(() => {
85
+ setScale(1);
86
+ setTranslate({ x: 0, y: 0 });
87
+ }, []);
88
+ const isZoomed = Math.abs(scale - 1) > 1e-3 || translate.x !== 0 || translate.y !== 0;
89
+ const handleClick = useCallback((e) => {
90
+ if (didDragRef.current) {
91
+ e.stopPropagation();
92
+ }
93
+ }, []);
94
+ const stopPropagation = useCallback((e) => {
95
+ e.stopPropagation();
96
+ }, []);
97
+ const diagramContent = /* @__PURE__ */ jsxs(
98
+ "div",
99
+ {
100
+ role: "presentation",
101
+ className: cn("tw:group/zoom tw:relative", isFullscreen && "tw:h-full"),
102
+ onClick: handleClick,
103
+ children: [
104
+ /* @__PURE__ */ jsx(
105
+ "div",
106
+ {
107
+ role: "presentation",
108
+ ref: containerRef,
109
+ className: cn(
110
+ "tw:overflow-hidden tw:cursor-grab tw:active:cursor-grabbing tw:p-3",
111
+ isFullscreen && "tw:h-full"
112
+ ),
113
+ onMouseDown: handleMouseDown,
114
+ onMouseMove: handleMouseMove,
115
+ onMouseUp: handleMouseUp,
116
+ onMouseLeave: handleMouseUp,
117
+ children: /* @__PURE__ */ jsx(
118
+ "div",
119
+ {
120
+ className: cn(
121
+ "tw:flex tw:justify-center tw:origin-center",
122
+ isFullscreen && "tw:h-full tw:[&>svg]:h-full"
123
+ ),
124
+ style: { transform: `translate(${translate.x}px, ${translate.y}px) scale(${scale})` },
125
+ dangerouslySetInnerHTML: { __html: safeHtml }
126
+ }
127
+ )
128
+ }
129
+ ),
130
+ /* @__PURE__ */ jsxs(
131
+ "div",
132
+ {
133
+ role: "presentation",
134
+ className: "tw:absolute tw:right-1 tw:top-1 tw:flex tw:gap-1 tw:opacity-0 tw:transition-opacity tw:focus-within:opacity-100 tw:group-hover/zoom:opacity-100",
135
+ onClick: stopPropagation,
136
+ children: [
137
+ isZoomed && /* @__PURE__ */ jsx(Tooltip, { tooltip: "Reset zoom", placement: "top", triggerAsChild: true, children: /* @__PURE__ */ jsx(Button, { size: "small", variant: "outline", onClick: handleReset, children: "Reset" }) }),
138
+ copyText && /* @__PURE__ */ jsx(ClipboardButton, { text: copyText, tooltip: "Copy code", size: "icon-sm", variant: "outline" }),
139
+ renderActions?.(safeHtml),
140
+ /* @__PURE__ */ jsx(
141
+ Tooltip,
142
+ {
143
+ tooltip: isFullscreen ? "Exit fullscreen" : "Fullscreen",
144
+ placement: "top",
145
+ triggerAsChild: true,
146
+ children: /* @__PURE__ */ jsx(
147
+ Button,
148
+ {
149
+ size: "icon-sm",
150
+ variant: "outline",
151
+ "aria-label": isFullscreen ? "Exit fullscreen" : "Fullscreen",
152
+ onClick: () => setIsFullscreen((f) => !f),
153
+ children: isFullscreen ? /* @__PURE__ */ jsx(Minimize2, { className: "tw:size-4" }) : /* @__PURE__ */ jsx(Maximize2, { className: "tw:size-4" })
154
+ }
155
+ )
156
+ }
157
+ )
158
+ ]
159
+ }
160
+ )
161
+ ]
162
+ }
163
+ );
164
+ if (isFullscreen) {
165
+ return createPortal(
166
+ /* @__PURE__ */ jsx(
167
+ "div",
168
+ {
169
+ role: "presentation",
170
+ "data-testid": "zoomable-svg-fullscreen",
171
+ className: "tw:fixed tw:inset-0 tw:z-[1060] tw:bg-muted tw:p-4 tailwindcss-preflight",
172
+ onClick: stopPropagation,
173
+ children: diagramContent
174
+ }
175
+ ),
176
+ document.body
177
+ );
178
+ }
179
+ return /* @__PURE__ */ jsx(
180
+ "div",
181
+ {
182
+ className: cn(
183
+ "tw:overflow-hidden tw:rounded-md tw:border tw:border-border tw:bg-muted",
184
+ className
185
+ ),
186
+ children: diagramContent
187
+ }
188
+ );
189
+ };
190
+
191
+ // src/components/ZoomableSvg/exportSvg.ts
192
+ var PNG_SCALE = 2;
193
+ var ROOT_SVG_TAG = /^<svg\b[^>]*>/i;
194
+ var HAS_DEFAULT_NAMESPACE = /\bxmlns\s*=/i;
195
+ var withSvgNamespace = (svg) => {
196
+ const trimmed = svg.trim();
197
+ const rootTag = trimmed.match(ROOT_SVG_TAG)?.[0];
198
+ if (!rootTag || HAS_DEFAULT_NAMESPACE.test(rootTag)) return trimmed;
199
+ return trimmed.replace(/^<svg\b/i, '<svg xmlns="http://www.w3.org/2000/svg"');
200
+ };
201
+ var toSvgFileContent = (svg) => `<?xml version="1.0" encoding="UTF-8"?>
202
+ ${withSvgNamespace(svg).trim()}
203
+ `;
204
+ var NUMBER = String.raw`\d+(?:\.\d+)?|\.\d+`;
205
+ var PIXEL_LENGTH = new RegExp(String.raw`^\s*(?:${NUMBER})(?:px)?\s*$`, "i");
206
+ var parsePixelLength = (value) => {
207
+ if (!PIXEL_LENGTH.test(value ?? "")) return null;
208
+ const parsed = Number.parseFloat(value ?? "");
209
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
210
+ };
211
+ var parseSvgSize = (svg) => {
212
+ const doc = new DOMParser().parseFromString(withSvgNamespace(svg), "image/svg+xml");
213
+ const element = doc.documentElement;
214
+ const viewBox = element.getAttribute("viewBox")?.split(/[\s,]+/).map(Number);
215
+ if (viewBox?.length === 4 && viewBox.every((value) => Number.isFinite(value))) {
216
+ const [, , width2, height2] = viewBox;
217
+ if (width2 !== void 0 && height2 !== void 0 && width2 > 0 && height2 > 0) {
218
+ return { width: width2, height: height2 };
219
+ }
220
+ }
221
+ const width = parsePixelLength(element.getAttribute("width"));
222
+ const height = parsePixelLength(element.getAttribute("height"));
223
+ return width !== null && height !== null ? { width, height } : null;
224
+ };
225
+ var CHANNEL_TRIPLET = new RegExp(String.raw`^(?:${NUMBER})\s+(?:${NUMBER})\s+(?:${NUMBER})$`);
226
+ var toCanvasColor = (token) => {
227
+ const trimmed = token.trim();
228
+ if (!trimmed) return "white";
229
+ return CHANNEL_TRIPLET.test(trimmed) ? `rgb(${trimmed})` : trimmed;
230
+ };
231
+ var resolveBackground = () => toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--color-background"));
232
+ var svgToPngBlob = async (svg, background) => {
233
+ const size = parseSvgSize(svg);
234
+ if (!size) throw new Error("Cannot determine diagram dimensions.");
235
+ const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(toSvgFileContent(svg))}`;
236
+ const image = new Image();
237
+ image.decoding = "sync";
238
+ await new Promise((resolve, reject) => {
239
+ image.onload = () => resolve();
240
+ image.onerror = () => reject(new Error("Failed to load the diagram for export."));
241
+ image.src = encoded;
242
+ });
243
+ const canvas = document.createElement("canvas");
244
+ canvas.width = Math.round(size.width * PNG_SCALE);
245
+ canvas.height = Math.round(size.height * PNG_SCALE);
246
+ const context = canvas.getContext("2d");
247
+ if (!context) throw new Error("Canvas is unavailable in this browser.");
248
+ context.fillStyle = background ?? resolveBackground();
249
+ context.fillRect(0, 0, canvas.width, canvas.height);
250
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
251
+ return new Promise((resolve, reject) => {
252
+ canvas.toBlob(
253
+ (blob) => blob ? resolve(blob) : reject(new Error("Failed to encode the PNG.")),
254
+ "image/png"
255
+ );
256
+ });
257
+ };
258
+
259
+ export { DiagramSkeleton, ZoomableSvg, svgToPngBlob, toSvgFileContent };
260
+ //# sourceMappingURL=chunk-IJTHFNNJ.js.map
261
+ //# sourceMappingURL=chunk-IJTHFNNJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/blocks/MermaidDiagram/DiagramSkeleton.tsx","../src/components/ZoomableSvg/ZoomableSvg.tsx","../src/components/ZoomableSvg/exportSvg.ts"],"names":["jsx","width","height"],"mappings":"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","file":"chunk-IJTHFNNJ.js","sourcesContent":["import { Skeleton } from '../../components/Skeleton/Skeleton';\n\n// Lives apart from MermaidDiagram so callers can show it while that component's\n// chunk is still loading, without importing beautiful-mermaid to do it.\nexport const DiagramSkeleton = () => (\n <div className=\"tw:my-3 tw:overflow-hidden tw:rounded-md tw:border tw:border-neutral-150 tw:bg-neutral-50 tw:p-6\">\n <Skeleton className=\"tw:h-40\" />\n </div>\n);\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\nimport { createPortal } from 'react-dom';\nimport DOMPurify from 'dompurify';\n\nimport { ClipboardButton } from '../../blocks/ClipboardButton';\nimport { Maximize2, Minimize2 } from '../../icons';\nimport { cn } from '../../utils';\nimport { Button } from '../Button';\nimport { Tooltip } from '../Tooltip';\n\nconst MIN_SCALE = 0.5;\nconst MAX_SCALE = 3;\nconst ZOOM_STEP = 0.1;\n\ntype Props = {\n /** Raw SVG markup. Sanitized internally with DOMPurify (SVG profile) before rendering, so it is\n * safe even when it originates from user/LLM-controlled content. Non-SVG HTML is stripped. */\n html: string;\n copyText?: string;\n className?: string;\n /**\n * Opt in to keeping `<foreignObject>` elements through sanitization. Off by\n * default — the SVG profile drops `<foreignObject>` so no HTML subtree can\n * survive. Only enable when the caller pre-flattens each `<foreignObject>` to\n * bare text (the SVG profile still strips any HTML children even when the\n * element itself is allowed). `MermaidDiagram` sets this for node/icon labels.\n */\n allowForeignObject?: boolean;\n /**\n * Extra toolbar controls, rendered between the copy and fullscreen buttons.\n *\n * Injected rather than built in, so export behaviour — filenames,\n * rasterization, error reporting — stays with the consumer and this component\n * keeps shipping in the lazy `@keboola/design/mermaid` chunk with no download\n * dependencies of its own. Pass `ExportMenu` here. Receives the sanitized\n * SVG so an exporter writes exactly what is on screen.\n */\n renderActions?: (safeSvg: string) => ReactNode;\n};\n\nexport const ZoomableSvg = ({\n html,\n copyText,\n className,\n allowForeignObject,\n renderActions,\n}: Props) => {\n const safeHtml = useMemo(\n () =>\n DOMPurify.sanitize(html, {\n USE_PROFILES: { svg: true, svgFilters: true },\n ...(allowForeignObject ? { ADD_TAGS: ['foreignObject'] } : {}),\n }),\n [html, allowForeignObject],\n );\n\n const containerRef = useRef<HTMLDivElement>(null);\n const [scale, setScale] = useState(1);\n const [translate, setTranslate] = useState({ x: 0, y: 0 });\n const [isFullscreen, setIsFullscreen] = useState(false);\n\n useEffect(() => {\n if (!isFullscreen) return;\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n setIsFullscreen(false);\n }\n };\n document.addEventListener('keydown', onKeyDown, { capture: true });\n return () => document.removeEventListener('keydown', onKeyDown, { capture: true });\n }, [isFullscreen]);\n\n const dragRef = useRef<{\n startX: number;\n startY: number;\n startTx: number;\n startTy: number;\n } | null>(null);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n\n const onWheel = (e: WheelEvent) => {\n if (!e.ctrlKey && !e.metaKey) return;\n e.preventDefault();\n const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;\n setScale((s) => Math.min(MAX_SCALE, Math.max(MIN_SCALE, s + delta)));\n };\n\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => el.removeEventListener('wheel', onWheel);\n // Re-bind when toggling fullscreen: the portal/inline switch remounts the\n // container element, so the listener must attach to the new node.\n }, [isFullscreen]);\n\n const didDragRef = useRef(false);\n\n const handleMouseDown = useCallback(\n (e: React.MouseEvent) => {\n if (e.button !== 0) return;\n e.preventDefault();\n didDragRef.current = false;\n dragRef.current = {\n startX: e.clientX,\n startY: e.clientY,\n startTx: translate.x,\n startTy: translate.y,\n };\n },\n [translate],\n );\n\n const handleMouseMove = useCallback((e: React.MouseEvent) => {\n if (!dragRef.current) return;\n didDragRef.current = true;\n setTranslate({\n x: dragRef.current.startTx + e.clientX - dragRef.current.startX,\n y: dragRef.current.startTy + e.clientY - dragRef.current.startY,\n });\n }, []);\n\n const handleMouseUp = useCallback(() => {\n dragRef.current = null;\n }, []);\n\n const handleReset = useCallback(() => {\n setScale(1);\n setTranslate({ x: 0, y: 0 });\n }, []);\n\n const isZoomed = Math.abs(scale - 1) > 0.001 || translate.x !== 0 || translate.y !== 0;\n\n const handleClick = useCallback((e: React.MouseEvent) => {\n if (didDragRef.current) {\n e.stopPropagation();\n }\n }, []);\n\n const stopPropagation = useCallback((e: React.SyntheticEvent) => {\n e.stopPropagation();\n }, []);\n\n const diagramContent = (\n <div\n role=\"presentation\"\n className={cn('tw:group/zoom tw:relative', isFullscreen && 'tw:h-full')}\n onClick={handleClick}\n >\n <div\n role=\"presentation\"\n ref={containerRef}\n className={cn(\n 'tw:overflow-hidden tw:cursor-grab tw:active:cursor-grabbing tw:p-3',\n isFullscreen && 'tw:h-full',\n )}\n onMouseDown={handleMouseDown}\n onMouseMove={handleMouseMove}\n onMouseUp={handleMouseUp}\n onMouseLeave={handleMouseUp}\n >\n <div\n className={cn(\n 'tw:flex tw:justify-center tw:origin-center',\n isFullscreen && 'tw:h-full tw:[&>svg]:h-full',\n )}\n style={{ transform: `translate(${translate.x}px, ${translate.y}px) scale(${scale})` }}\n dangerouslySetInnerHTML={{ __html: safeHtml }}\n />\n </div>\n <div\n role=\"presentation\"\n // `focus-within` as well as hover: the toolbar is opacity-0 but still in\n // the tab order, so without this a keyboard user tabs through invisible\n // controls with no visible focus ring.\n className=\"tw:absolute tw:right-1 tw:top-1 tw:flex tw:gap-1 tw:opacity-0 tw:transition-opacity tw:focus-within:opacity-100 tw:group-hover/zoom:opacity-100\"\n onClick={stopPropagation}\n >\n {isZoomed && (\n <Tooltip tooltip=\"Reset zoom\" placement=\"top\" triggerAsChild>\n <Button size=\"small\" variant=\"outline\" onClick={handleReset}>\n Reset\n </Button>\n </Tooltip>\n )}\n {copyText && (\n <ClipboardButton text={copyText} tooltip=\"Copy code\" size=\"icon-sm\" variant=\"outline\" />\n )}\n {renderActions?.(safeHtml)}\n <Tooltip\n tooltip={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}\n placement=\"top\"\n triggerAsChild\n >\n <Button\n size=\"icon-sm\"\n variant=\"outline\"\n aria-label={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}\n onClick={() => setIsFullscreen((f) => !f)}\n >\n {isFullscreen ? (\n <Minimize2 className=\"tw:size-4\" />\n ) : (\n <Maximize2 className=\"tw:size-4\" />\n )}\n </Button>\n </Tooltip>\n </div>\n </div>\n );\n\n if (isFullscreen) {\n return createPortal(\n <div\n role=\"presentation\"\n data-testid=\"zoomable-svg-fullscreen\"\n className=\"tw:fixed tw:inset-0 tw:z-[1060] tw:bg-muted tw:p-4 tailwindcss-preflight\"\n onClick={stopPropagation}\n >\n {diagramContent}\n </div>,\n document.body,\n );\n }\n\n return (\n <div\n className={cn(\n 'tw:overflow-hidden tw:rounded-md tw:border tw:border-border tw:bg-muted',\n className,\n )}\n >\n {diagramContent}\n </div>\n );\n};\n","const PNG_SCALE = 2;\n\n// The root `<svg …>` tag, so the namespace check looks only at its attributes.\n// A plain `includes('xmlns=')` would also match a nested element or body text,\n// and would miss `xmlns = \"…\"` — adding a second declaration, which parsers\n// reject outright with \"Attribute xmlns redefined\".\nconst ROOT_SVG_TAG = /^<svg\\b[^>]*>/i;\nconst HAS_DEFAULT_NAMESPACE = /\\bxmlns\\s*=/i;\n\n// A standalone SVG file needs the namespace declaration; mermaid's inline output\n// omits it because an inline <svg> inherits it from the host document.\n// Trim before matching: the anchored `^<svg` misses markup that arrives with a\n// leading newline or indentation, which would emit a standalone file with no\n// namespace — unopenable in an image viewer.\nconst withSvgNamespace = (svg: string) => {\n const trimmed = svg.trim();\n const rootTag = trimmed.match(ROOT_SVG_TAG)?.[0];\n\n if (!rootTag || HAS_DEFAULT_NAMESPACE.test(rootTag)) return trimmed;\n\n return trimmed.replace(/^<svg\\b/i, '<svg xmlns=\"http://www.w3.org/2000/svg\"');\n};\n\nexport const toSvgFileContent = (svg: string) =>\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n${withSvgNamespace(svg).trim()}\\n`;\n\n// A single decimal number: `12`, `12.5`, `.5`. Deliberately stricter than\n// `[\\d.]+`, which also matches `1.2.3` — and `parseFloat` truncates that to\n// `1.2` rather than rejecting it, so a malformed value would pass as a plausible\n// one.\nconst NUMBER = String.raw`\\d+(?:\\.\\d+)?|\\.\\d+`;\n\n// `parseFloat` happily reads a relative length as a number — `\"100%\"` becomes\n// `100`, `\"2em\"` becomes `2` — and `makeResponsive` sets `width=\"100%\"` on every\n// mermaid root, so an SVG with no usable viewBox would rasterize 100px wide with\n// no error. Accept only a unitless number or an explicit `px`.\nconst PIXEL_LENGTH = new RegExp(String.raw`^\\s*(?:${NUMBER})(?:px)?\\s*$`, 'i');\n\nconst parsePixelLength = (value: string | null): number | null => {\n if (!PIXEL_LENGTH.test(value ?? '')) return null;\n\n const parsed = Number.parseFloat(value ?? '');\n\n return Number.isFinite(parsed) && parsed > 0 ? parsed : null;\n};\n\nexport const parseSvgSize = (svg: string) => {\n const doc = new DOMParser().parseFromString(withSvgNamespace(svg), 'image/svg+xml');\n const element = doc.documentElement;\n\n const viewBox = element\n .getAttribute('viewBox')\n ?.split(/[\\s,]+/)\n .map(Number);\n // `> 0`, not truthiness: a negative extent is accepted by a truthy check, and\n // negative canvas dimensions silently fall back to the 300x150 default — an\n // export at the wrong size with no error. Falling through to width/height is\n // the better answer, since those are usually still valid.\n if (viewBox?.length === 4 && viewBox.every((value) => Number.isFinite(value))) {\n const [, , width, height] = viewBox;\n if (width !== undefined && height !== undefined && width > 0 && height > 0) {\n return { width, height };\n }\n }\n\n const width = parsePixelLength(element.getAttribute('width'));\n const height = parsePixelLength(element.getAttribute('height'));\n\n return width !== null && height !== null ? { width, height } : null;\n};\n\n// Resolve the themed page background so the exported PNG follows the active\n// brand and color scheme instead of a frozen literal. Falls back to `white`\n// (a CSS keyword, not a brand-locked hex) when the token is unset.\n//\n// The token holds Tailwind's bare channel triplet (\"249 250 251\") so utilities\n// can add an alpha. Canvas `fillStyle` rejects that form *silently* and keeps\n// its previous value (black), so wrap it back into rgb() before use.\nconst CHANNEL_TRIPLET = new RegExp(String.raw`^(?:${NUMBER})\\s+(?:${NUMBER})\\s+(?:${NUMBER})$`);\n\nexport const toCanvasColor = (token: string) => {\n const trimmed = token.trim();\n if (!trimmed) return 'white';\n\n // Only wrap a well-formed triplet. A malformed one (`1.2.3 4 5`) would produce\n // an invalid `rgb()` that `fillStyle` rejects silently, painting the PNG on\n // whatever colour was set before — passing it through unwrapped at least lets\n // a genuine CSS color value work.\n return CHANNEL_TRIPLET.test(trimmed) ? `rgb(${trimmed})` : trimmed;\n};\n\nconst resolveBackground = () =>\n toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue('--color-background'));\n\n/**\n * Rasterize SVG markup to a PNG blob at 2x for legible text.\n *\n * The SVG is loaded as a data URL rather than a blob URL: an <img> loading SVG\n * is treated as a foreign resource, so a blob URL would taint the canvas and\n * make toBlob throw a SecurityError. A background is painted first because a\n * transparent PNG reads as unusable when pasted into a slide deck.\n */\nexport const svgToPngBlob = async (svg: string, background?: string): Promise<Blob> => {\n const size = parseSvgSize(svg);\n if (!size) throw new Error('Cannot determine diagram dimensions.');\n\n const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(toSvgFileContent(svg))}`;\n\n const image = new Image();\n image.decoding = 'sync';\n await new Promise<void>((resolve, reject) => {\n image.onload = () => resolve();\n image.onerror = () => reject(new Error('Failed to load the diagram for export.'));\n image.src = encoded;\n });\n\n const canvas = document.createElement('canvas');\n canvas.width = Math.round(size.width * PNG_SCALE);\n canvas.height = Math.round(size.height * PNG_SCALE);\n\n const context = canvas.getContext('2d');\n if (!context) throw new Error('Canvas is unavailable in this browser.');\n\n context.fillStyle = background ?? resolveBackground();\n context.fillRect(0, 0, canvas.width, canvas.height);\n context.drawImage(image, 0, 0, canvas.width, canvas.height);\n\n return new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error('Failed to encode the PNG.'))),\n 'image/png',\n );\n });\n};\n"]}
@@ -0,0 +1,40 @@
1
+ import { clsx } from 'clsx';
2
+ import { extendTailwindMerge } from 'tailwind-merge';
3
+
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __commonJS = (cb, mod) => function __require() {
11
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
+ };
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+ var __copyProps = (to, from, except, desc) => {
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (let key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key) && key !== except)
21
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
26
+ // If the importer is in node compatibility mode or this is not an ESM
27
+ // file that has been converted to a CommonJS file using a Babel-
28
+ // compatible transform (i.e. "__esModule" has not been set), then set
29
+ // "default" to the CommonJS "module.exports" for node compatibility.
30
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
31
+ mod
32
+ ));
33
+ var twMerge = extendTailwindMerge({
34
+ prefix: "tw"
35
+ });
36
+ var cn = (...classes) => twMerge(clsx(...classes));
37
+
38
+ export { __commonJS, __export, __toESM, cn };
39
+ //# sourceMappingURL=chunk-JP7PNHLH.js.map
40
+ //# sourceMappingURL=chunk-JP7PNHLH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/classNames.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,IAAM,UAAU,mBAAA,CAAoB;AAAA,EAClC,MAAA,EAAQ;AACV,CAAC,CAAA;AAEM,IAAM,KAAK,CAAA,GAAI,OAAA,KAA0B,QAAQ,IAAA,CAAK,GAAG,OAAO,CAAC","file":"chunk-JP7PNHLH.js","sourcesContent":["import { type ClassValue, clsx } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\n// tailwind-merge v3 (Tailwind v4): the prefix drops the trailing separator —\n// `tw:` → `tw` (the `:` separator is implied).\nconst twMerge = extendTailwindMerge({\n prefix: 'tw',\n});\n\nexport const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes));\n"]}
@@ -0,0 +1,229 @@
1
+ import { ZoomableSvg, DiagramSkeleton } from './chunk-IJTHFNNJ.js';
2
+ import { cn } from './chunk-JP7PNHLH.js';
3
+ import { useId, useState, useRef, useEffect } from 'react';
4
+ import { renderMermaidSVG } from 'beautiful-mermaid';
5
+ import colors from '@keboola/tailwind-config/colors';
6
+ import { jsx } from 'react/jsx-runtime';
7
+
8
+ // src/blocks/MermaidDiagram/svgTransforms.ts
9
+ var HTML_TAG = /<[^>]+>/g;
10
+ var FOREIGN_OBJECT = /(<foreignObject\b[^>]*>)([\s\S]*?)(<\/foreignObject>)/g;
11
+ var ROOT_SVG_TAG = /^(\s*<svg\b)([^>]*)(>)/i;
12
+ var flattenForeignObjectLabels = (svg) => svg.replace(FOREIGN_OBJECT, (_match, open, inner, close) => {
13
+ const text = inner.replace(HTML_TAG, " ").replace(/\s+/g, " ").trim();
14
+ return `${open}${text}${close}`;
15
+ });
16
+ var STYLE_ATTR = /\s+style="([^"]*)"/i;
17
+ var makeResponsive = (svgString) => {
18
+ const viewBoxMatch = svgString.match(/viewBox="[^"]*\s([\d.]+)\s[\d.]+"/);
19
+ const vbWidth = viewBoxMatch ? Math.round(Number(viewBoxMatch[1])) : void 0;
20
+ return svgString.replace(ROOT_SVG_TAG, (_match, open, attrs, close) => {
21
+ const existingStyle = attrs.match(STYLE_ATTR)?.[1] ?? "";
22
+ const styleDecls = existingStyle.replace(/max-width:\s*[^;"]*;?\s*/gi, "").replace(/\s+/g, " ").trim().replace(/;$/, "");
23
+ let nextAttrs = attrs.replace(/\s+width="[^"]*"/i, "").replace(/\s+height="[^"]*"/i, "").replace(STYLE_ATTR, "");
24
+ nextAttrs += ' width="100%"';
25
+ const styleParts = [styleDecls, vbWidth ? `max-width: ${vbWidth}px` : ""].filter(Boolean).join("; ");
26
+ if (styleParts) nextAttrs += ` style="${styleParts}"`;
27
+ return `${open}${nextAttrs}${close}`;
28
+ });
29
+ };
30
+ var PRIMARY_FALLBACK_CHANNELS = "31 143 255";
31
+ var resolvePrimary = () => {
32
+ const channels = typeof document === "undefined" ? PRIMARY_FALLBACK_CHANNELS : getComputedStyle(document.documentElement).getPropertyValue("--primary").trim() || PRIMARY_FALLBACK_CHANNELS;
33
+ return `rgb(${channels})`;
34
+ };
35
+ var mermaidPromise = null;
36
+ var registerLucideIconPack = (m) => {
37
+ m.registerIconPacks([
38
+ {
39
+ name: "lucide",
40
+ loader: () => import('@iconify-json/lucide/icons.json').then((mod) => mod.default)
41
+ }
42
+ ]);
43
+ };
44
+ var getMermaid = () => {
45
+ if (!mermaidPromise) {
46
+ mermaidPromise = import('mermaid').then((m) => {
47
+ registerLucideIconPack(m.default);
48
+ m.default.initialize({
49
+ startOnLoad: false,
50
+ securityLevel: "strict",
51
+ theme: "base",
52
+ themeVariables: {
53
+ primaryColor: colors.blue[100],
54
+ primaryTextColor: colors.neutral[800],
55
+ primaryBorderColor: colors.blue[700],
56
+ secondaryColor: colors.white,
57
+ secondaryTextColor: colors.neutral[800],
58
+ secondaryBorderColor: colors.neutral[150],
59
+ tertiaryColor: colors.warning[100],
60
+ tertiaryTextColor: colors.neutral[800],
61
+ tertiaryBorderColor: colors.warning[500],
62
+ lineColor: colors.neutral[300],
63
+ textColor: colors.neutral[800],
64
+ mainBkg: colors.blue[100],
65
+ nodeBorder: colors.blue[700],
66
+ clusterBkg: colors.white,
67
+ clusterBorder: colors.neutral[150],
68
+ titleColor: colors.neutral[800],
69
+ edgeLabelBackground: colors.white,
70
+ nodeTextColor: colors.neutral[800],
71
+ actorBorder: colors.blue[700],
72
+ actorBkg: colors.blue[100],
73
+ actorTextColor: colors.neutral[800],
74
+ actorLineColor: colors.neutral[300],
75
+ signalColor: colors.neutral[800],
76
+ signalTextColor: colors.neutral[800],
77
+ labelBoxBkgColor: colors.blue[100],
78
+ labelBoxBorderColor: colors.blue[700],
79
+ labelTextColor: colors.neutral[800],
80
+ loopTextColor: colors.neutral[800],
81
+ noteBorderColor: colors.warning[500],
82
+ noteBkgColor: colors.warning[100],
83
+ noteTextColor: colors.neutral[800],
84
+ sectionBkgColor: colors.blue[100],
85
+ altSectionBkgColor: colors.white,
86
+ sectionBkgColor2: colors.neutral[100],
87
+ taskBorderColor: colors.blue[700],
88
+ taskBkgColor: colors.blue[100],
89
+ taskTextColor: colors.neutral[800],
90
+ taskTextDarkColor: colors.neutral[800],
91
+ taskTextLightColor: colors.neutral[800],
92
+ activeTaskBorderColor: resolvePrimary(),
93
+ activeTaskBkgColor: colors.blue[200],
94
+ doneTaskBorderColor: colors.green[600],
95
+ doneTaskBkgColor: colors.green[100],
96
+ critBorderColor: colors.error[500],
97
+ critBkgColor: colors.error[100],
98
+ fontFamily: "Inter, system-ui, sans-serif",
99
+ fontSize: "14px"
100
+ }
101
+ });
102
+ return m;
103
+ }).catch((err) => {
104
+ mermaidPromise = null;
105
+ throw err;
106
+ });
107
+ }
108
+ return mermaidPromise;
109
+ };
110
+ var stripFontImports = (svg) => svg.replace(/@import\s+url\([^)]*fonts\.googleapis\.com[^)]*\);?\s*/g, "");
111
+ var hasIconSyntax = (code) => /@\{[^}]*\bicon\s*:/.test(code) || /\b(?:icon|service|group)\b[^\n(]*\(\s*[\w-]+:/.test(code) || /\bfa[bslr]?:fa-/.test(code);
112
+ var tryFastRender = (code) => {
113
+ if (hasIconSyntax(code)) return null;
114
+ try {
115
+ return stripFontImports(
116
+ renderMermaidSVG(code, {
117
+ bg: colors.white,
118
+ fg: colors.neutral[800],
119
+ transparent: true,
120
+ font: "Inter, system-ui, sans-serif"
121
+ })
122
+ );
123
+ } catch (err) {
124
+ console.debug("[MermaidDiagram] fast render unavailable, using async fallback:", err);
125
+ return null;
126
+ }
127
+ };
128
+ var RENDER_DEBOUNCE_MS = 300;
129
+ var svgCache = /* @__PURE__ */ new Map();
130
+ var resolveSyncSvg = (code) => {
131
+ const cached = svgCache.get(code);
132
+ if (cached) return cached;
133
+ const fast = tryFastRender(code);
134
+ if (fast) svgCache.set(code, fast);
135
+ return fast;
136
+ };
137
+ var MermaidDiagram = ({ code, renderActions }) => {
138
+ const id = useId();
139
+ const [svg, setSvg] = useState(() => resolveSyncSvg(code));
140
+ const [renderFailed, setRenderFailed] = useState(false);
141
+ const debounceRef = useRef(null);
142
+ const renderIdRef = useRef(0);
143
+ const [prevCode, setPrevCode] = useState(code);
144
+ if (code !== prevCode) {
145
+ setPrevCode(code);
146
+ setSvg(resolveSyncSvg(code));
147
+ setRenderFailed(false);
148
+ }
149
+ useEffect(() => {
150
+ if (svgCache.has(code)) return;
151
+ const currentRenderId = ++renderIdRef.current;
152
+ if (debounceRef.current) {
153
+ clearTimeout(debounceRef.current);
154
+ }
155
+ debounceRef.current = setTimeout(() => {
156
+ getMermaid().then(async (mermaidModule) => {
157
+ if (renderIdRef.current !== currentRenderId) return;
158
+ const rendererId = `mermaid-${id.replaceAll(":", "")}`;
159
+ const container = document.createElement("div");
160
+ container.style.visibility = "hidden";
161
+ container.style.position = "absolute";
162
+ container.style.top = "-9999px";
163
+ container.style.width = "900px";
164
+ document.body.appendChild(container);
165
+ try {
166
+ const result = await mermaidModule.default.render(rendererId, code, container);
167
+ if (renderIdRef.current === currentRenderId && result) {
168
+ const svgOutput = flattenForeignObjectLabels(result.svg);
169
+ svgCache.set(code, svgOutput);
170
+ setSvg(svgOutput);
171
+ setRenderFailed(false);
172
+ }
173
+ } finally {
174
+ container.remove();
175
+ }
176
+ }).catch((err) => {
177
+ if (renderIdRef.current === currentRenderId) {
178
+ console.error("[MermaidDiagram] fallback failed:", err);
179
+ setSvg(null);
180
+ setRenderFailed(true);
181
+ }
182
+ });
183
+ }, RENDER_DEBOUNCE_MS);
184
+ const timer = debounceRef.current;
185
+ const renderId = renderIdRef;
186
+ return () => {
187
+ renderId.current++;
188
+ if (timer) {
189
+ clearTimeout(timer);
190
+ }
191
+ };
192
+ }, [code, id]);
193
+ const processedSvg = svg ? makeResponsive(svg) : null;
194
+ if (renderFailed) {
195
+ return /* @__PURE__ */ jsx(
196
+ "pre",
197
+ {
198
+ className: cn(
199
+ "tw:my-3 tw:overflow-auto tw:whitespace-pre-wrap tw:break-all",
200
+ "tw:py-3 tw:px-4",
201
+ "tw:bg-neutral-50",
202
+ "tw:rounded-md tw:text-neutral-400",
203
+ "tw:text-sm tw:font-mono"
204
+ ),
205
+ children: /* @__PURE__ */ jsx("code", { children: code })
206
+ }
207
+ );
208
+ }
209
+ if (processedSvg) {
210
+ return /* @__PURE__ */ jsx(
211
+ ZoomableSvg,
212
+ {
213
+ html: processedSvg,
214
+ copyText: `\`\`\`mermaid
215
+ ${code}
216
+ \`\`\``,
217
+ className: "tw:my-3",
218
+ allowForeignObject: true,
219
+ renderActions
220
+ },
221
+ code
222
+ );
223
+ }
224
+ return /* @__PURE__ */ jsx(DiagramSkeleton, {});
225
+ };
226
+
227
+ export { MermaidDiagram };
228
+ //# sourceMappingURL=chunk-KPNAW5MU.js.map
229
+ //# sourceMappingURL=chunk-KPNAW5MU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/blocks/MermaidDiagram/svgTransforms.ts","../src/blocks/MermaidDiagram/MermaidDiagram.tsx"],"names":[],"mappings":"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","file":"chunk-KPNAW5MU.js","sourcesContent":["// Pure post-processing of mermaid's SVG output, before it reaches ZoomableSvg's\n// DOMPurify pass. Extracted for unit testing.\n\nconst HTML_TAG = /<[^>]+>/g;\nconst FOREIGN_OBJECT = /(<foreignObject\\b[^>]*>)([\\s\\S]*?)(<\\/foreignObject>)/g;\n// The root <svg> open tag, anchored to the start of the string.\nconst ROOT_SVG_TAG = /^(\\s*<svg\\b)([^>]*)(>)/i;\n\n/**\n * Mermaid renders node labels — including icon-shape (`@{ icon: … }`) labels — as\n * HTML (`<div><span><p>…</p></span></div>`) inside an SVG `<foreignObject>`. The\n * ZoomableSvg DOMPurify pass runs the SVG namespace profile, which strips every\n * XHTML-namespaced element under an `<svg>` root, so those wrapped labels vanish\n * (icon rendered, text gone). DOMPurify keeps *bare text* directly inside a\n * `<foreignObject>`, so collapse each label's HTML to its text content. Mermaid\n * has already sanitized that text under `securityLevel: 'strict'`, and dropping\n * the wrapper tags is strictly safer than keeping them.\n *\n * Entities are left encoded (`&amp;`, `&lt;`, …): as text-node content inside the\n * `<foreignObject>` the browser renders them correctly, and decoding them here\n * would risk turning `&lt;`/`&gt;` back into real angle brackets that could form\n * markup in the sanitized SVG.\n */\nexport const flattenForeignObjectLabels = (svg: string): string =>\n svg.replace(FOREIGN_OBJECT, (_match, open: string, inner: string, close: string) => {\n const text = inner.replace(HTML_TAG, ' ').replace(/\\s+/g, ' ').trim();\n return `${open}${text}${close}`;\n });\n\n/**\n * Make the diagram fluid: strip the root `<svg>`'s fixed width/height/style and\n * set `width=\"100%\"` plus a `max-width` from the viewBox width so it never\n * overflows its container.\n *\n * Only the ROOT `<svg>` tag is rewritten (anchored to string start). Icon-shape\n * nodes embed their own inner `<svg>` icons with fixed width/height; an earlier\n * unanchored regex matched the first `<svg …height…>` anywhere, so when the root\n * tag carried no height (mermaid emits `width=\"100%\"` alone) it stripped the\n * FIRST icon's height instead — detaching and mis-sizing that icon.\n *\n * The existing `style` attribute is preserved (only an existing `max-width` is\n * replaced): beautiful-mermaid carries theme CSS vars (`--bg`, `--fg`) there that\n * its inner `<style>` block derives node/text colors from — dropping the whole\n * attribute made every derived color resolve to black.\n */\nconst STYLE_ATTR = /\\s+style=\"([^\"]*)\"/i;\n\nexport const makeResponsive = (svgString: string): string => {\n const viewBoxMatch = svgString.match(/viewBox=\"[^\"]*\\s([\\d.]+)\\s[\\d.]+\"/);\n const vbWidth = viewBoxMatch ? Math.round(Number(viewBoxMatch[1])) : undefined;\n\n return svgString.replace(ROOT_SVG_TAG, (_match, open: string, attrs: string, close: string) => {\n const existingStyle = attrs.match(STYLE_ATTR)?.[1] ?? '';\n const styleDecls = existingStyle\n .replace(/max-width:\\s*[^;\"]*;?\\s*/gi, '')\n .replace(/\\s+/g, ' ')\n .trim()\n .replace(/;$/, '');\n\n let nextAttrs = attrs\n .replace(/\\s+width=\"[^\"]*\"/i, '')\n .replace(/\\s+height=\"[^\"]*\"/i, '')\n .replace(STYLE_ATTR, '');\n\n nextAttrs += ' width=\"100%\"';\n\n const styleParts = [styleDecls, vbWidth ? `max-width: ${vbWidth}px` : '']\n .filter(Boolean)\n .join('; ');\n if (styleParts) nextAttrs += ` style=\"${styleParts}\"`;\n\n return `${open}${nextAttrs}${close}`;\n });\n};\n","import { useEffect, useId, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\nimport { renderMermaidSVG } from 'beautiful-mermaid';\nimport type mermaid from 'mermaid';\n\nimport colors from '@keboola/tailwind-config/colors';\n\nimport { ZoomableSvg } from '../../components/ZoomableSvg';\nimport { cn } from '../../utils';\n\nimport { DiagramSkeleton } from './DiagramSkeleton';\nimport { flattenForeignObjectLabels, makeResponsive } from './svgTransforms';\n\ntype MermaidModule = { default: typeof mermaid };\n\nconst PRIMARY_FALLBACK_CHANNELS = '31 143 255';\n\nconst resolvePrimary = (): string => {\n const channels =\n typeof document === 'undefined'\n ? PRIMARY_FALLBACK_CHANNELS\n : getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() ||\n PRIMARY_FALLBACK_CHANNELS;\n return `rgb(${channels})`;\n};\n\nlet mermaidPromise: Promise<MermaidModule> | null = null;\n\n// Iconify icon pack registered with mermaid so `@{ icon: \"lucide:name\" }` node\n// shapes and `architecture-beta service <id>(lucide:name)` render — keeping\n// diagram glyphs consistent with the Lucide icons used across the rest of the UI.\n// The pack JSON resolves from a workspace dependency (no CDN fetch) via a dynamic\n// import, so it lands in its own lazy chunk and icons render offline / under a\n// strict CSP without weighing down the initial bundle.\nconst registerLucideIconPack = (m: typeof mermaid): void => {\n m.registerIconPacks([\n {\n name: 'lucide',\n loader: () => import('@iconify-json/lucide/icons.json').then((mod) => mod.default),\n },\n ]);\n};\n\nconst getMermaid = (): Promise<MermaidModule> => {\n if (!mermaidPromise) {\n mermaidPromise = import('mermaid')\n .then((m) => {\n registerLucideIconPack(m.default);\n m.default.initialize({\n startOnLoad: false,\n securityLevel: 'strict',\n theme: 'base',\n themeVariables: {\n primaryColor: colors.blue[100],\n primaryTextColor: colors.neutral[800],\n primaryBorderColor: colors.blue[700],\n secondaryColor: colors.white,\n secondaryTextColor: colors.neutral[800],\n secondaryBorderColor: colors.neutral[150],\n tertiaryColor: colors.warning[100],\n tertiaryTextColor: colors.neutral[800],\n tertiaryBorderColor: colors.warning[500],\n lineColor: colors.neutral[300],\n textColor: colors.neutral[800],\n mainBkg: colors.blue[100],\n nodeBorder: colors.blue[700],\n clusterBkg: colors.white,\n clusterBorder: colors.neutral[150],\n titleColor: colors.neutral[800],\n edgeLabelBackground: colors.white,\n nodeTextColor: colors.neutral[800],\n actorBorder: colors.blue[700],\n actorBkg: colors.blue[100],\n actorTextColor: colors.neutral[800],\n actorLineColor: colors.neutral[300],\n signalColor: colors.neutral[800],\n signalTextColor: colors.neutral[800],\n labelBoxBkgColor: colors.blue[100],\n labelBoxBorderColor: colors.blue[700],\n labelTextColor: colors.neutral[800],\n loopTextColor: colors.neutral[800],\n noteBorderColor: colors.warning[500],\n noteBkgColor: colors.warning[100],\n noteTextColor: colors.neutral[800],\n sectionBkgColor: colors.blue[100],\n altSectionBkgColor: colors.white,\n sectionBkgColor2: colors.neutral[100],\n taskBorderColor: colors.blue[700],\n taskBkgColor: colors.blue[100],\n taskTextColor: colors.neutral[800],\n taskTextDarkColor: colors.neutral[800],\n taskTextLightColor: colors.neutral[800],\n activeTaskBorderColor: resolvePrimary(),\n activeTaskBkgColor: colors.blue[200],\n doneTaskBorderColor: colors.green[600],\n doneTaskBkgColor: colors.green[100],\n critBorderColor: colors.error[500],\n critBkgColor: colors.error[100],\n fontFamily: 'Inter, system-ui, sans-serif',\n fontSize: '14px',\n },\n });\n return m;\n })\n .catch((err) => {\n mermaidPromise = null;\n throw err;\n });\n }\n return mermaidPromise;\n};\n\nconst stripFontImports = (svg: string): string =>\n svg.replace(/@import\\s+url\\([^)]*fonts\\.googleapis\\.com[^)]*\\);?\\s*/g, '');\n\n// beautiful-mermaid's synchronous renderer has no icon-pack support, so a diagram\n// using icon syntax must skip the fast path and take the async mermaid render\n// (which registers the pack). Matches:\n// - the flowchart / treeView icon shape `@{ icon: \"pack:name\" }`\n// - `architecture-beta` / `treeView` refs `icon|service|group <id>(pack:name)`\n// (the id between keyword and `(` is optional)\n// - legacy FontAwesome refs `fa:fa-*` / `fab:` embedded in node text\nconst hasIconSyntax = (code: string): boolean =>\n /@\\{[^}]*\\bicon\\s*:/.test(code) ||\n /\\b(?:icon|service|group)\\b[^\\n(]*\\(\\s*[\\w-]+:/.test(code) ||\n /\\bfa[bslr]?:fa-/.test(code);\n\nconst tryFastRender = (code: string): string | null => {\n if (hasIconSyntax(code)) return null;\n try {\n return stripFontImports(\n renderMermaidSVG(code, {\n bg: colors.white,\n fg: colors.neutral[800],\n transparent: true,\n font: 'Inter, system-ui, sans-serif',\n }),\n );\n } catch (err) {\n // beautiful-mermaid can't parse every mermaid dialect; the async mermaid\n // renderer is the fallback. Log at debug so regressions are diagnosable\n // without noising up the console on the expected unsupported-syntax path.\n console.debug('[MermaidDiagram] fast render unavailable, using async fallback:', err);\n return null;\n }\n};\n\nconst RENDER_DEBOUNCE_MS = 300;\n\n// Module-level SVG cache — persists across component remounts (e.g. tab switches\n// that cause React to remount the tree). Same code always produces the same SVG.\nconst svgCache = new Map<string, string>();\n\n// Resolve an SVG synchronously: cache hit first, otherwise a fast synchronous\n// render. Caches the result and returns it, or null when neither is available\n// (the caller then falls back to the async mermaid render).\nconst resolveSyncSvg = (code: string): string | null => {\n const cached = svgCache.get(code);\n if (cached) return cached;\n const fast = tryFastRender(code);\n if (fast) svgCache.set(code, fast);\n return fast;\n};\n\ntype Props = {\n /** Mermaid diagram source code. Supports flowchart, sequence, class, state, ER,\n * pie, gantt, and mindmap. Gantt `dateFormat HH:mm` (time-only) is not supported\n * by mermaid's dayjs parser — use `dateFormat YYYY-MM-DD HH:mm` with `axisFormat %H:%M` instead. */\n code: string;\n /** Extra toolbar controls (e.g. an `ExportMenu`), forwarded to `ZoomableSvg`.\n * Receives the sanitized SVG. Omit to keep the default toolbar. */\n renderActions?: (safeSvg: string) => ReactNode;\n};\n\nexport const MermaidDiagram = ({ code, renderActions }: Props) => {\n const id = useId();\n const [svg, setSvg] = useState<string | null>(() => resolveSyncSvg(code));\n const [renderFailed, setRenderFailed] = useState(false);\n const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const renderIdRef = useRef(0);\n\n // Synchronously reset stale state when code changes to avoid one-frame flash\n const [prevCode, setPrevCode] = useState(code);\n if (code !== prevCode) {\n setPrevCode(code);\n setSvg(resolveSyncSvg(code));\n setRenderFailed(false);\n }\n\n useEffect(() => {\n // Synchronous results (cache hit / fast render) are already applied and\n // cached by the useState initializer (mount) and the render-phase reset\n // (code change). Only the async fallback needs an effect.\n if (svgCache.has(code)) return;\n\n const currentRenderId = ++renderIdRef.current;\n\n if (debounceRef.current) {\n clearTimeout(debounceRef.current);\n }\n\n debounceRef.current = setTimeout(() => {\n getMermaid()\n .then(async (mermaidModule) => {\n if (renderIdRef.current !== currentRenderId) return;\n const rendererId = `mermaid-${id.replaceAll(':', '')}`;\n const container = document.createElement('div');\n container.style.visibility = 'hidden';\n container.style.position = 'absolute';\n container.style.top = '-9999px';\n container.style.width = '900px';\n document.body.appendChild(container);\n try {\n const result = await mermaidModule.default.render(rendererId, code, container);\n if (renderIdRef.current === currentRenderId && result) {\n const svgOutput = flattenForeignObjectLabels(result.svg);\n svgCache.set(code, svgOutput);\n setSvg(svgOutput);\n setRenderFailed(false);\n }\n } finally {\n container.remove();\n }\n })\n .catch((err) => {\n if (renderIdRef.current === currentRenderId) {\n console.error('[MermaidDiagram] fallback failed:', err);\n setSvg(null);\n setRenderFailed(true);\n }\n });\n }, RENDER_DEBOUNCE_MS);\n\n const timer = debounceRef.current;\n const renderId = renderIdRef;\n return () => {\n renderId.current++;\n if (timer) {\n clearTimeout(timer);\n }\n };\n }, [code, id]);\n\n const processedSvg = svg ? makeResponsive(svg) : null;\n\n if (renderFailed) {\n return (\n <pre\n className={cn(\n 'tw:my-3 tw:overflow-auto tw:whitespace-pre-wrap tw:break-all',\n 'tw:py-3 tw:px-4',\n 'tw:bg-neutral-50',\n 'tw:rounded-md tw:text-neutral-400',\n 'tw:text-sm tw:font-mono',\n )}\n >\n <code>{code}</code>\n </pre>\n );\n }\n\n if (processedSvg) {\n return (\n <ZoomableSvg\n key={code}\n html={processedSvg}\n copyText={`\\`\\`\\`mermaid\\n${code}\\n\\`\\`\\``}\n className=\"tw:my-3\"\n allowForeignObject\n renderActions={renderActions}\n />\n );\n }\n\n return <DiagramSkeleton />;\n};\n"]}