@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.
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/dist/CodeBlock-CVlrvVPP.mjs +159 -0
- package/dist/CodeBlock-Cymx1ZAY.cjs +176 -0
- package/dist/CodeBlock.cjs +5 -0
- package/dist/CodeBlock.d.cts +2 -0
- package/dist/CodeBlock.d.mts +2 -0
- package/dist/CodeBlock.mjs +2 -0
- package/dist/Control.cjs +61 -0
- package/dist/Control.d.cts +18 -0
- package/dist/Control.d.mts +18 -0
- package/dist/Control.mjs +60 -0
- package/dist/Editor.cjs +129 -0
- package/dist/Editor.d.cts +40 -0
- package/dist/Editor.d.mts +40 -0
- package/dist/Editor.mjs +128 -0
- package/dist/MarkdownContext.cjs +13 -0
- package/dist/MarkdownContext.d.cts +2 -0
- package/dist/MarkdownContext.d.mts +2 -0
- package/dist/MarkdownContext.mjs +11 -0
- package/dist/Mermaid-BclI9A7e.mjs +231 -0
- package/dist/Mermaid-DqP2Q0bt.cjs +236 -0
- package/dist/Mermaid.cjs +3 -0
- package/dist/Mermaid.d.cts +2 -0
- package/dist/Mermaid.d.mts +2 -0
- package/dist/Mermaid.mjs +2 -0
- package/dist/Preview-BVT70uXa.cjs +276 -0
- package/dist/Preview-C1CwRDj5.mjs +246 -0
- package/dist/Preview.cjs +3 -0
- package/dist/Preview.d.cts +53 -0
- package/dist/Preview.d.mts +53 -0
- package/dist/Preview.mjs +2 -0
- package/dist/Toolbar-Ba3l-oBU.cjs +600 -0
- package/dist/Toolbar-C0SNI-JK.mjs +589 -0
- package/dist/Toolbar.cjs +4 -0
- package/dist/Toolbar.d.cts +13 -0
- package/dist/Toolbar.d.mts +13 -0
- package/dist/Toolbar.mjs +2 -0
- package/dist/applyAction-BDkgYhOd.cjs +120 -0
- package/dist/applyAction-BGWMbUYT.mjs +115 -0
- package/dist/index-BO0LyUUG.d.cts +27 -0
- package/dist/index-BO0LyUUG.d.mts +27 -0
- package/dist/index-BtMTBOcM.d.cts +48 -0
- package/dist/index-BtMTBOcM.d.mts +48 -0
- package/dist/index-rGZIhUgj.d.cts +76 -0
- package/dist/index-rGZIhUgj.d.mts +76 -0
- package/dist/index.cjs +19 -0
- package/dist/index.d.cts +50 -0
- package/dist/index.d.mts +50 -0
- package/dist/index.mjs +8 -0
- package/package.json +110 -0
package/dist/Control.mjs
ADDED
|
@@ -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 };
|
package/dist/Editor.cjs
ADDED
|
@@ -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 };
|
package/dist/Editor.mjs
ADDED
|
@@ -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,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 };
|