@soroush.tech/markdown 0.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -0
  3. package/dist/CodeBlock-CVlrvVPP.mjs +159 -0
  4. package/dist/CodeBlock-Cymx1ZAY.cjs +176 -0
  5. package/dist/CodeBlock.cjs +5 -0
  6. package/dist/CodeBlock.d.cts +2 -0
  7. package/dist/CodeBlock.d.mts +2 -0
  8. package/dist/CodeBlock.mjs +2 -0
  9. package/dist/Control.cjs +61 -0
  10. package/dist/Control.d.cts +18 -0
  11. package/dist/Control.d.mts +18 -0
  12. package/dist/Control.mjs +60 -0
  13. package/dist/Editor.cjs +129 -0
  14. package/dist/Editor.d.cts +40 -0
  15. package/dist/Editor.d.mts +40 -0
  16. package/dist/Editor.mjs +128 -0
  17. package/dist/MarkdownContext.cjs +13 -0
  18. package/dist/MarkdownContext.d.cts +2 -0
  19. package/dist/MarkdownContext.d.mts +2 -0
  20. package/dist/MarkdownContext.mjs +11 -0
  21. package/dist/Mermaid-BclI9A7e.mjs +231 -0
  22. package/dist/Mermaid-DqP2Q0bt.cjs +236 -0
  23. package/dist/Mermaid.cjs +3 -0
  24. package/dist/Mermaid.d.cts +2 -0
  25. package/dist/Mermaid.d.mts +2 -0
  26. package/dist/Mermaid.mjs +2 -0
  27. package/dist/Preview-BVT70uXa.cjs +276 -0
  28. package/dist/Preview-C1CwRDj5.mjs +246 -0
  29. package/dist/Preview.cjs +3 -0
  30. package/dist/Preview.d.cts +53 -0
  31. package/dist/Preview.d.mts +53 -0
  32. package/dist/Preview.mjs +2 -0
  33. package/dist/Toolbar-Ba3l-oBU.cjs +600 -0
  34. package/dist/Toolbar-C0SNI-JK.mjs +589 -0
  35. package/dist/Toolbar.cjs +4 -0
  36. package/dist/Toolbar.d.cts +13 -0
  37. package/dist/Toolbar.d.mts +13 -0
  38. package/dist/Toolbar.mjs +2 -0
  39. package/dist/applyAction-BDkgYhOd.cjs +120 -0
  40. package/dist/applyAction-BGWMbUYT.mjs +115 -0
  41. package/dist/index-BO0LyUUG.d.cts +27 -0
  42. package/dist/index-BO0LyUUG.d.mts +27 -0
  43. package/dist/index-BtMTBOcM.d.cts +48 -0
  44. package/dist/index-BtMTBOcM.d.mts +48 -0
  45. package/dist/index-rGZIhUgj.d.cts +76 -0
  46. package/dist/index-rGZIhUgj.d.mts +76 -0
  47. package/dist/index.cjs +19 -0
  48. package/dist/index.d.cts +50 -0
  49. package/dist/index.d.mts +50 -0
  50. package/dist/index.mjs +8 -0
  51. package/package.json +110 -0
@@ -0,0 +1,60 @@
1
+ import { t as applyAction } from "./applyAction-BGWMbUYT.mjs";
2
+ import { MarkdownContext } from "./MarkdownContext.mjs";
3
+ import { useMemo, useRef } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/Control/Control.tsx
6
+ /**
7
+ * The headless root of the Markdown compound: owns the shared value/selection state and exposes
8
+ * it (plus a `dispatch` for toolbar actions) via context. It renders no UI of its own — the
9
+ * consumer arranges the parts and decides which panes to show.
10
+ */
11
+ function Control({ value, onChange, children }) {
12
+ const lastSelectionRef = useRef(null);
13
+ const pendingSelectionRef = useRef(null);
14
+ const context = useMemo(() => {
15
+ const dispatch = (action) => {
16
+ const { start, end } = lastSelectionRef.current ?? {
17
+ start: value.length,
18
+ end: value.length
19
+ };
20
+ const next = applyAction(action, {
21
+ value,
22
+ selectionStart: start,
23
+ selectionEnd: end
24
+ });
25
+ const selection = {
26
+ start: next.selectionStart,
27
+ end: next.selectionEnd
28
+ };
29
+ pendingSelectionRef.current = selection;
30
+ lastSelectionRef.current = selection;
31
+ onChange(next.value);
32
+ };
33
+ const rememberSelection = (selection) => {
34
+ lastSelectionRef.current = selection;
35
+ };
36
+ const queueSelection = (selection) => {
37
+ pendingSelectionRef.current = selection;
38
+ lastSelectionRef.current = selection;
39
+ };
40
+ const takeQueuedSelection = () => {
41
+ const selection = pendingSelectionRef.current;
42
+ pendingSelectionRef.current = null;
43
+ return selection;
44
+ };
45
+ return {
46
+ value,
47
+ onChange,
48
+ dispatch,
49
+ rememberSelection,
50
+ queueSelection,
51
+ takeQueuedSelection
52
+ };
53
+ }, [value, onChange]);
54
+ return /* @__PURE__ */ jsx(MarkdownContext.Provider, {
55
+ value: context,
56
+ children
57
+ });
58
+ }
59
+ //#endregion
60
+ export { Control };
@@ -0,0 +1,129 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_applyAction = require("./applyAction-BDkgYhOd.cjs");
3
+ const require_MarkdownContext = require("./MarkdownContext.cjs");
4
+ let react = require("react");
5
+ let react_jsx_runtime = require("react/jsx-runtime");
6
+ let _soroush_tech_design_system_Flex = require("@soroush.tech/design-system/Flex");
7
+ let _soroush_tech_design_system_Typography = require("@soroush.tech/design-system/Typography");
8
+ let _soroush_tech_design_system = require("@soroush.tech/design-system");
9
+ let _soroush_tech_design_system_theme = require("@soroush.tech/design-system/theme");
10
+ let _soroush_tech_design_system_TextInput = require("@soroush.tech/design-system/TextInput");
11
+ //#region src/Editor/Editor.tsx
12
+ const EditorRoot = (0, _soroush_tech_design_system.styled)(_soroush_tech_design_system_Flex.Flex, {
13
+ name: "MarkdownEditor",
14
+ label: "MarkdownEditor"
15
+ })();
16
+ const INDENT_ACTION = {
17
+ id: "indent",
18
+ label: "",
19
+ ariaLabel: "",
20
+ kind: "linePrefix",
21
+ linePrefix: " "
22
+ };
23
+ const noop = () => {};
24
+ /**
25
+ * The source `<textarea>`, with selection tracking and Tab-to-indent. Inside a `Control` it is
26
+ * driven by context (so the `Toolbar` can act on it); on its own it is a plain controlled field
27
+ * via `value`/`onChange`.
28
+ */
29
+ function Editor({ value: valueProp, onChange: onChangeProp, placeholder = "Write your article in Markdown…", minRows = 16, variant, color, textColor: textColorProp, size, name, id, error, required, disabled, showShortcutHint = true }) {
30
+ const context = (0, react.useContext)(require_MarkdownContext.MarkdownContext);
31
+ const theme = (0, _soroush_tech_design_system_theme.useTheme)();
32
+ const textColor = textColorProp ?? (0, _soroush_tech_design_system_theme.themeDefault)(theme, "textColor", "initial");
33
+ const hintId = (0, react.useId)();
34
+ const localPendingRef = (0, react.useRef)(null);
35
+ const value = context?.value ?? valueProp ?? "";
36
+ const onChange = context?.onChange ?? onChangeProp ?? noop;
37
+ const paneRef = (0, react.useRef)(null);
38
+ const queueSelection = (selection) => {
39
+ if (context) context.queueSelection(selection);
40
+ else localPendingRef.current = selection;
41
+ };
42
+ const takeQueuedSelection = () => {
43
+ if (context) return context.takeQueuedSelection();
44
+ const selection = localPendingRef.current;
45
+ localPendingRef.current = null;
46
+ return selection;
47
+ };
48
+ const rememberSelection = (event) => {
49
+ if (!context) return;
50
+ const { selectionStart, selectionEnd } = event.currentTarget;
51
+ context.rememberSelection({
52
+ start: selectionStart,
53
+ end: selectionEnd
54
+ });
55
+ };
56
+ const handleKeyDown = (event) => {
57
+ const el = event.currentTarget;
58
+ if (event.key === "Escape" || (event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === "m") {
59
+ event.preventDefault();
60
+ el.blur();
61
+ return;
62
+ }
63
+ if (event.key === "Tab" && !event.shiftKey) {
64
+ event.preventDefault();
65
+ const { selectionStart, selectionEnd } = el;
66
+ const next = value.slice(selectionStart, selectionEnd).includes("\n") ? require_applyAction.applyAction(INDENT_ACTION, {
67
+ value,
68
+ selectionStart,
69
+ selectionEnd
70
+ }) : {
71
+ value: value.slice(0, selectionStart) + " " + value.slice(selectionEnd),
72
+ selectionStart: selectionStart + 1,
73
+ selectionEnd: selectionStart + 1
74
+ };
75
+ queueSelection({
76
+ start: next.selectionStart,
77
+ end: next.selectionEnd
78
+ });
79
+ onChange(next.value);
80
+ }
81
+ };
82
+ (0, react.useLayoutEffect)(() => {
83
+ const selection = takeQueuedSelection();
84
+ if (!selection) return;
85
+ const textarea = paneRef.current.querySelector("textarea");
86
+ textarea.focus();
87
+ textarea.setSelectionRange(selection.start, selection.end);
88
+ });
89
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(EditorRoot, {
90
+ ref: paneRef,
91
+ flexDirection: "column",
92
+ flex: 1,
93
+ minWidth: 0,
94
+ gap: 1,
95
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_soroush_tech_design_system_TextInput.TextInput, {
96
+ multiline: true,
97
+ resize: true,
98
+ fullWidth: true,
99
+ variant,
100
+ color,
101
+ textColor,
102
+ size,
103
+ name,
104
+ id,
105
+ error,
106
+ required,
107
+ disabled,
108
+ minRows,
109
+ value,
110
+ placeholder,
111
+ onChange: (event) => onChange(event.target.value),
112
+ inputProps: {
113
+ "aria-label": "Markdown source",
114
+ "aria-describedby": showShortcutHint ? hintId : void 0,
115
+ onSelect: rememberSelection,
116
+ onBlur: rememberSelection,
117
+ onKeyDown: handleKeyDown
118
+ }
119
+ }), showShortcutHint && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_soroush_tech_design_system_Typography.Typography, {
120
+ id: hintId,
121
+ variant: "caption",
122
+ color: "secondary",
123
+ m: 0,
124
+ children: "Tab inserts a tab · press Escape or Ctrl/Cmd+Shift+M to move focus out."
125
+ })]
126
+ });
127
+ }
128
+ //#endregion
129
+ exports.Editor = Editor;
@@ -0,0 +1,40 @@
1
+ import { TextInputColor, TextInputSize, TextInputTextColor, TextInputVariant } from "@soroush.tech/design-system/TextInput";
2
+ //#region src/Editor/Editor.d.ts
3
+ interface EditorProps {
4
+ /** Standalone source value — used when rendered outside a `Control`. Ignored inside one. */
5
+ value?: string;
6
+ /** Standalone change handler — used when rendered outside a `Control`. Ignored inside one. */
7
+ onChange?: (value: string) => void;
8
+ /** Placeholder shown while the source is empty. */
9
+ placeholder?: string;
10
+ /** Minimum visible rows of the source textarea. Default: `16`. */
11
+ minRows?: number;
12
+ /** Visual style of the source input — forwarded to `TextInput`. Default: `'default'`. */
13
+ variant?: TextInputVariant;
14
+ /** Focus/active border colour — forwarded to `TextInput`. */
15
+ color?: TextInputColor;
16
+ /** Text colour of the typed source — forwarded to `TextInput`. Default: `'initial'`. */
17
+ textColor?: TextInputTextColor;
18
+ /** Padding/font-size density — forwarded to `TextInput`. */
19
+ size?: TextInputSize;
20
+ /** Native `name` on the textarea — set it for native `<form>` submission / FormData. */
21
+ name?: string;
22
+ /** `id` for label association. Falls back to the enclosing `FormControl`. */
23
+ id?: string;
24
+ /** Marks the field invalid (error border). Falls back to the enclosing `FormControl`. */
25
+ error?: boolean;
26
+ /** Marks the field required. Falls back to the enclosing `FormControl`. */
27
+ required?: boolean;
28
+ /** Disables editing. Falls back to the enclosing `FormControl`. */
29
+ disabled?: boolean;
30
+ /** Shows a one-line hint about the Tab / focus-release shortcuts under the field. Default: `true`. */
31
+ showShortcutHint?: boolean;
32
+ }
33
+ /**
34
+ * The source `<textarea>`, with selection tracking and Tab-to-indent. Inside a `Control` it is
35
+ * driven by context (so the `Toolbar` can act on it); on its own it is a plain controlled field
36
+ * via `value`/`onChange`.
37
+ */
38
+ declare function Editor({ value: valueProp, onChange: onChangeProp, placeholder, minRows, variant, color, textColor: textColorProp, size, name, id, error, required, disabled, showShortcutHint }: Readonly<EditorProps>): import("react").JSX.Element;
39
+ //#endregion
40
+ export { Editor, EditorProps };
@@ -0,0 +1,40 @@
1
+ import { TextInputColor, TextInputSize, TextInputTextColor, TextInputVariant } from "@soroush.tech/design-system/TextInput";
2
+ //#region src/Editor/Editor.d.ts
3
+ interface EditorProps {
4
+ /** Standalone source value — used when rendered outside a `Control`. Ignored inside one. */
5
+ value?: string;
6
+ /** Standalone change handler — used when rendered outside a `Control`. Ignored inside one. */
7
+ onChange?: (value: string) => void;
8
+ /** Placeholder shown while the source is empty. */
9
+ placeholder?: string;
10
+ /** Minimum visible rows of the source textarea. Default: `16`. */
11
+ minRows?: number;
12
+ /** Visual style of the source input — forwarded to `TextInput`. Default: `'default'`. */
13
+ variant?: TextInputVariant;
14
+ /** Focus/active border colour — forwarded to `TextInput`. */
15
+ color?: TextInputColor;
16
+ /** Text colour of the typed source — forwarded to `TextInput`. Default: `'initial'`. */
17
+ textColor?: TextInputTextColor;
18
+ /** Padding/font-size density — forwarded to `TextInput`. */
19
+ size?: TextInputSize;
20
+ /** Native `name` on the textarea — set it for native `<form>` submission / FormData. */
21
+ name?: string;
22
+ /** `id` for label association. Falls back to the enclosing `FormControl`. */
23
+ id?: string;
24
+ /** Marks the field invalid (error border). Falls back to the enclosing `FormControl`. */
25
+ error?: boolean;
26
+ /** Marks the field required. Falls back to the enclosing `FormControl`. */
27
+ required?: boolean;
28
+ /** Disables editing. Falls back to the enclosing `FormControl`. */
29
+ disabled?: boolean;
30
+ /** Shows a one-line hint about the Tab / focus-release shortcuts under the field. Default: `true`. */
31
+ showShortcutHint?: boolean;
32
+ }
33
+ /**
34
+ * The source `<textarea>`, with selection tracking and Tab-to-indent. Inside a `Control` it is
35
+ * driven by context (so the `Toolbar` can act on it); on its own it is a plain controlled field
36
+ * via `value`/`onChange`.
37
+ */
38
+ declare function Editor({ value: valueProp, onChange: onChangeProp, placeholder, minRows, variant, color, textColor: textColorProp, size, name, id, error, required, disabled, showShortcutHint }: Readonly<EditorProps>): import("react").JSX.Element;
39
+ //#endregion
40
+ export { Editor, EditorProps };
@@ -0,0 +1,128 @@
1
+ import { t as applyAction } from "./applyAction-BGWMbUYT.mjs";
2
+ import { MarkdownContext } from "./MarkdownContext.mjs";
3
+ import { useContext, useId, useLayoutEffect, useRef } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { Flex } from "@soroush.tech/design-system/Flex";
6
+ import { Typography } from "@soroush.tech/design-system/Typography";
7
+ import { styled } from "@soroush.tech/design-system";
8
+ import { themeDefault, useTheme } from "@soroush.tech/design-system/theme";
9
+ import { TextInput } from "@soroush.tech/design-system/TextInput";
10
+ //#region src/Editor/Editor.tsx
11
+ const EditorRoot = styled(Flex, {
12
+ name: "MarkdownEditor",
13
+ label: "MarkdownEditor"
14
+ })();
15
+ const INDENT_ACTION = {
16
+ id: "indent",
17
+ label: "",
18
+ ariaLabel: "",
19
+ kind: "linePrefix",
20
+ linePrefix: " "
21
+ };
22
+ const noop = () => {};
23
+ /**
24
+ * The source `<textarea>`, with selection tracking and Tab-to-indent. Inside a `Control` it is
25
+ * driven by context (so the `Toolbar` can act on it); on its own it is a plain controlled field
26
+ * via `value`/`onChange`.
27
+ */
28
+ function Editor({ value: valueProp, onChange: onChangeProp, placeholder = "Write your article in Markdown…", minRows = 16, variant, color, textColor: textColorProp, size, name, id, error, required, disabled, showShortcutHint = true }) {
29
+ const context = useContext(MarkdownContext);
30
+ const theme = useTheme();
31
+ const textColor = textColorProp ?? themeDefault(theme, "textColor", "initial");
32
+ const hintId = useId();
33
+ const localPendingRef = useRef(null);
34
+ const value = context?.value ?? valueProp ?? "";
35
+ const onChange = context?.onChange ?? onChangeProp ?? noop;
36
+ const paneRef = useRef(null);
37
+ const queueSelection = (selection) => {
38
+ if (context) context.queueSelection(selection);
39
+ else localPendingRef.current = selection;
40
+ };
41
+ const takeQueuedSelection = () => {
42
+ if (context) return context.takeQueuedSelection();
43
+ const selection = localPendingRef.current;
44
+ localPendingRef.current = null;
45
+ return selection;
46
+ };
47
+ const rememberSelection = (event) => {
48
+ if (!context) return;
49
+ const { selectionStart, selectionEnd } = event.currentTarget;
50
+ context.rememberSelection({
51
+ start: selectionStart,
52
+ end: selectionEnd
53
+ });
54
+ };
55
+ const handleKeyDown = (event) => {
56
+ const el = event.currentTarget;
57
+ if (event.key === "Escape" || (event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === "m") {
58
+ event.preventDefault();
59
+ el.blur();
60
+ return;
61
+ }
62
+ if (event.key === "Tab" && !event.shiftKey) {
63
+ event.preventDefault();
64
+ const { selectionStart, selectionEnd } = el;
65
+ const next = value.slice(selectionStart, selectionEnd).includes("\n") ? applyAction(INDENT_ACTION, {
66
+ value,
67
+ selectionStart,
68
+ selectionEnd
69
+ }) : {
70
+ value: value.slice(0, selectionStart) + " " + value.slice(selectionEnd),
71
+ selectionStart: selectionStart + 1,
72
+ selectionEnd: selectionStart + 1
73
+ };
74
+ queueSelection({
75
+ start: next.selectionStart,
76
+ end: next.selectionEnd
77
+ });
78
+ onChange(next.value);
79
+ }
80
+ };
81
+ useLayoutEffect(() => {
82
+ const selection = takeQueuedSelection();
83
+ if (!selection) return;
84
+ const textarea = paneRef.current.querySelector("textarea");
85
+ textarea.focus();
86
+ textarea.setSelectionRange(selection.start, selection.end);
87
+ });
88
+ return /* @__PURE__ */ jsxs(EditorRoot, {
89
+ ref: paneRef,
90
+ flexDirection: "column",
91
+ flex: 1,
92
+ minWidth: 0,
93
+ gap: 1,
94
+ children: [/* @__PURE__ */ jsx(TextInput, {
95
+ multiline: true,
96
+ resize: true,
97
+ fullWidth: true,
98
+ variant,
99
+ color,
100
+ textColor,
101
+ size,
102
+ name,
103
+ id,
104
+ error,
105
+ required,
106
+ disabled,
107
+ minRows,
108
+ value,
109
+ placeholder,
110
+ onChange: (event) => onChange(event.target.value),
111
+ inputProps: {
112
+ "aria-label": "Markdown source",
113
+ "aria-describedby": showShortcutHint ? hintId : void 0,
114
+ onSelect: rememberSelection,
115
+ onBlur: rememberSelection,
116
+ onKeyDown: handleKeyDown
117
+ }
118
+ }), showShortcutHint && /* @__PURE__ */ jsx(Typography, {
119
+ id: hintId,
120
+ variant: "caption",
121
+ color: "secondary",
122
+ m: 0,
123
+ children: "Tab inserts a tab · press Escape or Ctrl/Cmd+Shift+M to move focus out."
124
+ })]
125
+ });
126
+ }
127
+ //#endregion
128
+ export { Editor };
@@ -0,0 +1,13 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let react = require("react");
3
+ //#region src/MarkdownContext/MarkdownContext.ts
4
+ const MarkdownContext = (0, react.createContext)(null);
5
+ /** Reads the Markdown compound context; throws if used outside `<Markdown.Control>`. */
6
+ function useMarkdownContext() {
7
+ const context = (0, react.useContext)(MarkdownContext);
8
+ if (!context) throw new Error("Markdown parts must be rendered inside <Markdown.Control>");
9
+ return context;
10
+ }
11
+ //#endregion
12
+ exports.MarkdownContext = MarkdownContext;
13
+ exports.useMarkdownContext = useMarkdownContext;
@@ -0,0 +1,2 @@
1
+ import { i as useMarkdownContext, n as MarkdownContextValue, r as MarkdownSelection, t as MarkdownContext } from "./index-rGZIhUgj.cjs";
2
+ export { MarkdownContext, MarkdownContextValue, MarkdownSelection, useMarkdownContext };
@@ -0,0 +1,2 @@
1
+ import { i as useMarkdownContext, n as MarkdownContextValue, r as MarkdownSelection, t as MarkdownContext } from "./index-rGZIhUgj.mjs";
2
+ export { MarkdownContext, MarkdownContextValue, MarkdownSelection, useMarkdownContext };
@@ -0,0 +1,11 @@
1
+ import { createContext, useContext } from "react";
2
+ //#region src/MarkdownContext/MarkdownContext.ts
3
+ const MarkdownContext = createContext(null);
4
+ /** Reads the Markdown compound context; throws if used outside `<Markdown.Control>`. */
5
+ function useMarkdownContext() {
6
+ const context = useContext(MarkdownContext);
7
+ if (!context) throw new Error("Markdown parts must be rendered inside <Markdown.Control>");
8
+ return context;
9
+ }
10
+ //#endregion
11
+ export { MarkdownContext, useMarkdownContext };
@@ -0,0 +1,231 @@
1
+ import { r as CodeBlock } from "./CodeBlock-CVlrvVPP.mjs";
2
+ import { useEffect, useId, useRef, useState } from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ import { Button } from "@soroush.tech/design-system/Button";
5
+ import { Flex } from "@soroush.tech/design-system/Flex";
6
+ import { Icon } from "@soroush.tech/design-system/Icon";
7
+ import { useTheme } from "@soroush.tech/design-system/theme";
8
+ import { View } from "@soroush.tech/design-system/View";
9
+ import { Grid } from "@soroush.tech/design-system/Grid";
10
+ import { Modal } from "@soroush.tech/design-system/Modal";
11
+ //#region src/Mermaid/DiagramViewer.tsx
12
+ const MIN_SCALE = .2;
13
+ const MAX_SCALE = 5;
14
+ const ZOOM_STEP = .2;
15
+ const PAN_STEP = 48;
16
+ const MIN_HEIGHT = 180;
17
+ const MAX_HEIGHT = 640;
18
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
19
+ /**
20
+ * Wraps a rendered mermaid diagram in a zoom/pan viewport with a themed control cluster
21
+ * (bottom-right: a pan d-pad around a reset, plus zoom in/out) and an expand control
22
+ * (top-right) that opens the diagram in a fullscreen dialog. Drag-to-pan and wheel-to-zoom
23
+ * work too. Only the diagram layer clips; the controls sit above it, so a short diagram never
24
+ * hides them. Everything is a CSS transform — no dependency.
25
+ */
26
+ function DiagramViewer({ svg, expandable = true, fill = false }) {
27
+ const [scale, setScale] = useState(1);
28
+ const [offset, setOffset] = useState({
29
+ x: 0,
30
+ y: 0
31
+ });
32
+ const [dragging, setDragging] = useState(false);
33
+ const [fullscreen, setFullscreen] = useState(false);
34
+ const [naturalHeight, setNaturalHeight] = useState(0);
35
+ const dragOrigin = useRef(null);
36
+ const contentRef = useRef(null);
37
+ useEffect(() => {
38
+ setNaturalHeight(contentRef.current.offsetHeight);
39
+ }, [svg]);
40
+ const zoomBy = (delta) => setScale((current) => clamp(current + delta, MIN_SCALE, MAX_SCALE));
41
+ const panBy = (dx, dy) => setOffset((current) => ({
42
+ x: current.x + dx,
43
+ y: current.y + dy
44
+ }));
45
+ const reset = () => {
46
+ setScale(1);
47
+ setOffset({
48
+ x: 0,
49
+ y: 0
50
+ });
51
+ };
52
+ const closeFullscreen = () => setFullscreen(false);
53
+ const onWheel = (event) => {
54
+ event.preventDefault();
55
+ zoomBy(event.deltaY < 0 ? ZOOM_STEP : -.2);
56
+ };
57
+ const onMouseDown = (event) => {
58
+ dragOrigin.current = {
59
+ x: event.clientX - offset.x,
60
+ y: event.clientY - offset.y
61
+ };
62
+ setDragging(true);
63
+ };
64
+ const onMouseMove = (event) => {
65
+ if (!dragOrigin.current) return;
66
+ setOffset({
67
+ x: event.clientX - dragOrigin.current.x,
68
+ y: event.clientY - dragOrigin.current.y
69
+ });
70
+ };
71
+ const endDrag = () => {
72
+ dragOrigin.current = null;
73
+ setDragging(false);
74
+ };
75
+ const control = (label, icon, onClick) => /* @__PURE__ */ jsx(Button, {
76
+ type: "button",
77
+ variant: "outlined",
78
+ shape: "rounded",
79
+ size: "sm",
80
+ "aria-label": label,
81
+ onClick,
82
+ children: /* @__PURE__ */ jsx(Icon, {
83
+ name: icon,
84
+ size: "1.1rem"
85
+ })
86
+ }, label);
87
+ const inlineHeight = Math.max(MIN_HEIGHT, clamp(naturalHeight, MIN_HEIGHT, MAX_HEIGHT) * scale);
88
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(View, {
89
+ my: fill ? 0 : 2,
90
+ position: "relative",
91
+ maxWidth: "100%",
92
+ borderRadius: "md",
93
+ height: fill ? "100%" : void 0,
94
+ children: [
95
+ /* @__PURE__ */ jsx(Flex, {
96
+ onWheel: fill ? onWheel : void 0,
97
+ onMouseDown,
98
+ onMouseMove,
99
+ onMouseUp: endDrag,
100
+ onMouseLeave: endDrag,
101
+ overflow: "hidden",
102
+ borderRadius: "md",
103
+ alignItems: "center",
104
+ justifyContent: "center",
105
+ height: fill ? "100%" : `${inlineHeight}px`,
106
+ style: {
107
+ cursor: dragging ? "grabbing" : "grab",
108
+ userSelect: "none",
109
+ WebkitUserSelect: "none"
110
+ },
111
+ children: /* @__PURE__ */ jsx(View, {
112
+ ref: contentRef,
113
+ "data-testid": "diagram-transform",
114
+ style: {
115
+ transform: `translate(${offset.x}px, ${offset.y}px) scale(${scale})`,
116
+ transformOrigin: "center",
117
+ transition: dragging ? "none" : "transform 0.12s ease-out"
118
+ },
119
+ dangerouslySetInnerHTML: { __html: svg }
120
+ })
121
+ }),
122
+ expandable && /* @__PURE__ */ jsx(View, {
123
+ position: "absolute",
124
+ top: 1,
125
+ right: 1,
126
+ zIndex: 1,
127
+ children: control("Expand diagram", "fullscreen", () => setFullscreen(true))
128
+ }),
129
+ /* @__PURE__ */ jsxs(Grid, {
130
+ position: "absolute",
131
+ bottom: 1,
132
+ right: 1,
133
+ zIndex: 1,
134
+ gridTemplateColumns: "repeat(3, auto)",
135
+ gap: .5,
136
+ children: [
137
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true }),
138
+ control("Pan up", "expand_less", () => panBy(0, -48)),
139
+ control("Zoom in", "zoom_in", () => zoomBy(ZOOM_STEP)),
140
+ control("Pan left", "chevron_left", () => panBy(-48, 0)),
141
+ control("Reset view", "refresh", reset),
142
+ control("Pan right", "chevron_right", () => panBy(PAN_STEP, 0)),
143
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true }),
144
+ control("Pan down", "expand_more", () => panBy(0, PAN_STEP)),
145
+ control("Zoom out", "zoom_out", () => zoomBy(-.2))
146
+ ]
147
+ })
148
+ ]
149
+ }), expandable && /* @__PURE__ */ jsx(Modal, {
150
+ isOpen: fullscreen,
151
+ onClose: closeFullscreen,
152
+ children: /* @__PURE__ */ jsxs(Flex, {
153
+ bg: "paper",
154
+ role: "dialog",
155
+ "aria-label": "Diagram fullscreen",
156
+ position: "fixed",
157
+ top: 0,
158
+ right: 0,
159
+ bottom: 0,
160
+ left: 0,
161
+ p: 2,
162
+ children: [/* @__PURE__ */ jsx(View, {
163
+ position: "absolute",
164
+ top: 2,
165
+ right: 2,
166
+ zIndex: 1,
167
+ children: control("Close", "close", closeFullscreen)
168
+ }), /* @__PURE__ */ jsx(Flex, {
169
+ flex: 1,
170
+ minHeight: 0,
171
+ children: /* @__PURE__ */ jsx(DiagramViewer, {
172
+ svg,
173
+ expandable: false,
174
+ fill: true
175
+ })
176
+ })]
177
+ })
178
+ })] });
179
+ }
180
+ //#endregion
181
+ //#region src/Mermaid/Mermaid.tsx
182
+ async function toSvg(id, chart, theme) {
183
+ const { default: mermaid } = await import("mermaid");
184
+ mermaid.initialize({
185
+ startOnLoad: false,
186
+ securityLevel: "strict",
187
+ suppressErrorRendering: true,
188
+ theme: theme.colorScheme === "dark" ? "dark" : "default",
189
+ themeVariables: {
190
+ darkMode: theme.colorScheme === "dark",
191
+ background: theme.background.paper,
192
+ fontFamily: theme.fonts.body,
193
+ mainBkg: theme.background.primary,
194
+ ...theme.mermaid
195
+ }
196
+ });
197
+ const { svg } = await mermaid.render(id, chart);
198
+ return svg;
199
+ }
200
+ /**
201
+ * Renders a mermaid diagram. Until the client renders the SVG — and if the source is
202
+ * invalid — it falls back to the diagram source in a `CodeBlock`, so server output and
203
+ * no-JS readers still show something useful and hydration stays stable.
204
+ */
205
+ function Mermaid({ chart, diagram }) {
206
+ const theme = useTheme();
207
+ const id = `mermaid-${useId().replace(/[^a-zA-Z0-9-]/g, "")}`;
208
+ const [svg, setSvg] = useState(null);
209
+ useEffect(() => {
210
+ let active = true;
211
+ toSvg(id, chart, theme).then((rendered) => {
212
+ if (active) setSvg(rendered);
213
+ }).catch(() => {
214
+ if (active) setSvg(null);
215
+ });
216
+ return () => {
217
+ active = false;
218
+ };
219
+ }, [
220
+ chart,
221
+ id,
222
+ theme
223
+ ]);
224
+ if (svg) return /* @__PURE__ */ jsx(DiagramViewer, {
225
+ svg,
226
+ ...diagram
227
+ });
228
+ return /* @__PURE__ */ jsx(CodeBlock, { children: chart });
229
+ }
230
+ //#endregion
231
+ export { Mermaid as t };