leepi 0.0.0 → 0.0.3

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 (86) hide show
  1. package/dist/core/active-marks.d.ts +15 -0
  2. package/dist/core/active-marks.js +57 -0
  3. package/dist/core/commands.d.ts +39 -0
  4. package/dist/core/commands.js +415 -0
  5. package/dist/core/editor.d.ts +25 -0
  6. package/dist/core/editor.js +102 -0
  7. package/dist/core/field-notifier.d.ts +21 -0
  8. package/dist/core/field-notifier.js +56 -0
  9. package/dist/core/highlight-style.js +60 -0
  10. package/dist/core/highlight.d.ts +8 -0
  11. package/dist/core/highlight.js +34 -0
  12. package/dist/core/plugins/blockquote.d.ts +11 -0
  13. package/dist/core/plugins/blockquote.js +78 -0
  14. package/dist/core/plugins/bracket.d.ts +6 -0
  15. package/dist/core/plugins/bracket.js +38 -0
  16. package/dist/core/plugins/code-block.d.ts +27 -0
  17. package/dist/core/plugins/code-block.js +207 -0
  18. package/dist/core/plugins/heading.d.ts +13 -0
  19. package/dist/core/plugins/heading.js +111 -0
  20. package/dist/core/plugins/inline.d.ts +14 -0
  21. package/dist/core/plugins/inline.js +103 -0
  22. package/dist/core/plugins/link.d.ts +25 -0
  23. package/dist/core/plugins/link.js +104 -0
  24. package/dist/core/plugins/list.d.ts +14 -0
  25. package/dist/core/plugins/list.js +91 -0
  26. package/dist/core/plugins/table.d.ts +12 -0
  27. package/dist/core/plugins/table.js +161 -0
  28. package/dist/core/plugins.d.ts +9 -0
  29. package/dist/core/plugins.js +9 -0
  30. package/dist/core/popover.d.ts +9 -0
  31. package/dist/core/popover.js +16 -0
  32. package/dist/core/registry.d.ts +10 -0
  33. package/dist/core/registry.js +8 -0
  34. package/dist/core/types.d.ts +25 -0
  35. package/dist/core/types.js +0 -0
  36. package/dist/core/utils.d.ts +13 -0
  37. package/dist/core/utils.js +32 -0
  38. package/dist/leepi.css +461 -0
  39. package/dist/react/code-block-popover.d.ts +76 -0
  40. package/dist/react/code-block-popover.js +223 -0
  41. package/dist/react/context.d.ts +42 -0
  42. package/dist/react/context.js +88 -0
  43. package/dist/react/editor.d.ts +30 -0
  44. package/dist/react/editor.js +60 -0
  45. package/dist/react/floating-toolbar.d.ts +30 -0
  46. package/dist/react/floating-toolbar.js +87 -0
  47. package/dist/react/link-popover.d.ts +70 -0
  48. package/dist/react/link-popover.js +222 -0
  49. package/dist/react/preview.d.ts +13 -0
  50. package/dist/react/preview.js +56 -0
  51. package/dist/react/toolbar.d.ts +51 -0
  52. package/dist/react/toolbar.js +161 -0
  53. package/package.json +90 -1
  54. package/src/core/active-marks.ts +89 -0
  55. package/src/core/commands.ts +461 -0
  56. package/src/core/editor.ts +139 -0
  57. package/src/core/field-notifier.ts +71 -0
  58. package/src/core/highlight-style.ts +66 -0
  59. package/src/core/highlight.ts +50 -0
  60. package/src/core/plugins/blockquote.ts +108 -0
  61. package/src/core/plugins/bracket.ts +34 -0
  62. package/src/core/plugins/code-block.ts +195 -0
  63. package/src/core/plugins/heading.ts +95 -0
  64. package/src/core/plugins/index.ts +16 -0
  65. package/src/core/plugins/inline.ts +62 -0
  66. package/src/core/plugins/link.ts +124 -0
  67. package/src/core/plugins/list.ts +68 -0
  68. package/src/core/plugins/table.ts +217 -0
  69. package/src/core/popover.ts +17 -0
  70. package/src/core/registry.ts +18 -0
  71. package/src/core/types.ts +25 -0
  72. package/src/core/utils.ts +38 -0
  73. package/src/react/code-block-popover.tsx +387 -0
  74. package/src/react/context.tsx +153 -0
  75. package/src/react/editor.tsx +106 -0
  76. package/src/react/floating-toolbar.tsx +161 -0
  77. package/src/react/link-popover.tsx +354 -0
  78. package/src/react/preview.tsx +80 -0
  79. package/src/react/toolbar.tsx +294 -0
  80. package/src/styles/floating-toolbar.css +52 -0
  81. package/src/styles/leepi.css +2 -0
  82. package/src/styles/popover.css +93 -0
  83. package/src/styles/preview.css +191 -0
  84. package/src/styles/theme.css +99 -0
  85. package/src/styles/tokens.css +63 -0
  86. package/src/styles/toolbar.css +55 -0
@@ -0,0 +1,161 @@
1
+ import React, {
2
+ useEffect,
3
+ useState,
4
+ type ReactNode,
5
+ type ComponentProps,
6
+ type JSX,
7
+ useMemo,
8
+ } from "react";
9
+ import { Toolbar as BaseToolbar } from "@base-ui/react/toolbar";
10
+ import { useEditorContext, useStateField } from "./context";
11
+ import { popoverField } from "../core/popover";
12
+ import { isInsideCodeBlock } from "../core/utils";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Helpers
16
+ // ---------------------------------------------------------------------------
17
+
18
+ function useFloatingPosition() {
19
+ const { view } = useEditorContext();
20
+ const popoverReq = useStateField(popoverField, null);
21
+ const popoverOpen = popoverReq != null;
22
+ const [position, setPosition] = useState<{ top: number; left: number } | null>(null);
23
+
24
+ useEffect(() => {
25
+ if (!view) return;
26
+
27
+ const updatePosition = () => {
28
+ const { state } = view;
29
+ const { from, to } = state.selection.main;
30
+
31
+ if (from === to || isInsideCodeBlock(view)) {
32
+ setPosition(null);
33
+ return;
34
+ }
35
+
36
+ // Hide for multiline selections
37
+ const fromLine = state.doc.lineAt(from).number;
38
+ const toLine = state.doc.lineAt(to).number;
39
+ if (fromLine !== toLine) {
40
+ setPosition(null);
41
+ return;
42
+ }
43
+
44
+ const fromCoords = view.coordsAtPos(from);
45
+ const toCoords = view.coordsAtPos(to);
46
+ if (!fromCoords || !toCoords) {
47
+ setPosition(null);
48
+ return;
49
+ }
50
+
51
+ const left = (fromCoords.left + toCoords.left) / 2;
52
+ const top = Math.min(fromCoords.top, toCoords.top) - 8;
53
+
54
+ setPosition({ top, left });
55
+ };
56
+
57
+ const handler = () => requestAnimationFrame(updatePosition);
58
+ view.contentDOM.addEventListener("mouseup", handler);
59
+ view.contentDOM.addEventListener("keyup", handler);
60
+
61
+ // Re-evaluate position when a popover closes while a selection exists
62
+ if (!popoverOpen) {
63
+ updatePosition();
64
+ }
65
+
66
+ return () => {
67
+ view.contentDOM.removeEventListener("mouseup", handler);
68
+ view.contentDOM.removeEventListener("keyup", handler);
69
+ };
70
+ }, [view, popoverOpen]);
71
+
72
+ if (popoverOpen) return null;
73
+
74
+ return position;
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // FloatingToolbar.Button — generic passthrough
79
+ // ---------------------------------------------------------------------------
80
+
81
+ type FloatingToolbarButtonProps = ComponentProps<typeof BaseToolbar.Button>;
82
+
83
+ function FloatingToolbarButton({ className, ...props }: FloatingToolbarButtonProps): JSX.Element {
84
+ return <BaseToolbar.Button className={`lp-floating-toolbar-btn ${className || ""}`} {...props} />;
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // FloatingToolbar.Separator
89
+ // ---------------------------------------------------------------------------
90
+
91
+ type FloatingToolbarSeparatorProps = ComponentProps<typeof BaseToolbar.Separator>;
92
+
93
+ function FloatingToolbarSeparator({
94
+ className,
95
+ ...props
96
+ }: FloatingToolbarSeparatorProps): JSX.Element {
97
+ return (
98
+ <BaseToolbar.Separator
99
+ className={`lp-floating-toolbar-separator ${className || ""}`}
100
+ {...props}
101
+ />
102
+ );
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // FloatingToolbar.Group
107
+ // ---------------------------------------------------------------------------
108
+
109
+ type FloatingToolbarGroupProps = ComponentProps<typeof BaseToolbar.Group>;
110
+
111
+ function FloatingToolbarGroup({ className, ...props }: FloatingToolbarGroupProps): JSX.Element {
112
+ return (
113
+ <BaseToolbar.Group className={`lp-floating-toolbar-group ${className || ""}`} {...props} />
114
+ );
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // FloatingToolbar root
119
+ // ---------------------------------------------------------------------------
120
+
121
+ export type FloatingToolbarProps = {
122
+ className?: string;
123
+ children?: ReactNode;
124
+ } & Omit<ComponentProps<typeof BaseToolbar.Root>, "children">;
125
+
126
+ function FloatingToolbarRoot({
127
+ className,
128
+ children,
129
+ style: propStyle,
130
+ }: FloatingToolbarProps): JSX.Element | null {
131
+ const position = useFloatingPosition();
132
+ const style = useMemo(() => {
133
+ if (!position) return propStyle;
134
+ return {
135
+ ...propStyle,
136
+ position: "fixed",
137
+ top: position.top,
138
+ left: position.left,
139
+ transform: "translate(-50%, -100%)",
140
+ } satisfies React.CSSProperties;
141
+ }, [position, propStyle]);
142
+
143
+ if (!position) return null;
144
+
145
+ return (
146
+ <BaseToolbar.Root className={`lp-floating-toolbar ${className || ""}`} style={style}>
147
+ {children}
148
+ </BaseToolbar.Root>
149
+ );
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Namespace exports
154
+ // ---------------------------------------------------------------------------
155
+
156
+ export {
157
+ FloatingToolbarRoot as Root,
158
+ FloatingToolbarButton as Button,
159
+ FloatingToolbarSeparator as Separator,
160
+ FloatingToolbarGroup as Group,
161
+ };
@@ -0,0 +1,354 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useId,
6
+ useMemo,
7
+ useState,
8
+ type ReactNode,
9
+ type JSX,
10
+ } from "react";
11
+ import { EditorSelection } from "@codemirror/state";
12
+ import { Popover } from "@base-ui/react/popover";
13
+ import { Form } from "@base-ui/react/form";
14
+ import { Field } from "@base-ui/react/field";
15
+ import { useRender, type useRender as UseRender } from "@base-ui/react/use-render";
16
+ import { mergeProps } from "@base-ui/react/merge-props";
17
+ import { useEditorContext, useStateField, useVirtualAnchor } from "./context";
18
+ import { popoverField, closePopover } from "../core/popover";
19
+ import type { LinkRequest } from "../core/plugins/link";
20
+ import { insertLink } from "../core/commands";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Context
24
+ // ---------------------------------------------------------------------------
25
+
26
+ interface LinkPopoverContextValue {
27
+ label: string;
28
+ setLabel: (label: string) => void;
29
+ url: string;
30
+ setUrl: (url: string) => void;
31
+ isEditing: boolean;
32
+ onRemove: () => void;
33
+ onClose: () => void;
34
+ }
35
+
36
+ const LinkPopoverCtx = createContext<LinkPopoverContextValue | null>(null);
37
+
38
+ function useLinkPopoverCtx() {
39
+ const ctx = useContext(LinkPopoverCtx);
40
+ if (!ctx) {
41
+ throw new Error("leepi: LinkPopover sub-components must be used inside LinkPopover.Root");
42
+ }
43
+ return ctx;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Sub-components
48
+ // ---------------------------------------------------------------------------
49
+
50
+ interface LabelInputProps extends UseRender.ComponentProps<"input"> {
51
+ autoFocus?: boolean;
52
+ placeholder?: string;
53
+ children?: ReactNode;
54
+ }
55
+
56
+ function LabelInput({
57
+ autoFocus,
58
+ placeholder,
59
+ render,
60
+ children,
61
+ ...restProps
62
+ }: LabelInputProps): JSX.Element {
63
+ const { label, setLabel } = useLinkPopoverCtx();
64
+ const id = useId();
65
+
66
+ const element = useRender({
67
+ render,
68
+ defaultTagName: "input",
69
+ props: mergeProps<"input">(
70
+ {
71
+ id,
72
+ value: label,
73
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => setLabel(e.target.value),
74
+ autoFocus,
75
+ placeholder,
76
+ className: "lp-popover-input",
77
+ },
78
+ restProps,
79
+ ),
80
+ });
81
+
82
+ return (
83
+ <Field.Root className="lp-popover-field">
84
+ <Field.Label htmlFor={id} className="lp-popover-label">
85
+ {children}
86
+ </Field.Label>
87
+ {element}
88
+ </Field.Root>
89
+ );
90
+ }
91
+
92
+ interface UrlInputProps extends UseRender.ComponentProps<"input"> {
93
+ autoFocus?: boolean;
94
+ placeholder?: string;
95
+ children?: ReactNode;
96
+ }
97
+
98
+ function UrlInput({
99
+ autoFocus,
100
+ placeholder,
101
+ render,
102
+ children,
103
+ ...restProps
104
+ }: UrlInputProps): JSX.Element {
105
+ const { url, setUrl } = useLinkPopoverCtx();
106
+ const id = useId();
107
+
108
+ const element = useRender({
109
+ render,
110
+ defaultTagName: "input",
111
+ props: mergeProps<"input">(
112
+ {
113
+ id,
114
+ type: "url",
115
+ value: url,
116
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => setUrl(e.target.value),
117
+ autoFocus,
118
+ placeholder,
119
+ className: "lp-popover-input",
120
+ },
121
+ restProps,
122
+ ),
123
+ });
124
+
125
+ return (
126
+ <Field.Root className="lp-popover-field">
127
+ <Field.Label htmlFor={id} className="lp-popover-label">
128
+ {children}
129
+ </Field.Label>
130
+ {element}
131
+ </Field.Root>
132
+ );
133
+ }
134
+
135
+ function Actions({
136
+ cancelLabel,
137
+ submitLabel,
138
+ removeLabel,
139
+ }: {
140
+ cancelLabel?: ReactNode;
141
+ submitLabel?: ReactNode;
142
+ removeLabel?: ReactNode;
143
+ }): JSX.Element {
144
+ const { isEditing, url, onRemove, onClose } = useLinkPopoverCtx();
145
+ return (
146
+ <div className="lp-popover-actions">
147
+ {isEditing ? (
148
+ <button type="button" onClick={onRemove} className="lp-popover-btn lp-popover-btn--danger">
149
+ {removeLabel}
150
+ </button>
151
+ ) : (
152
+ <span />
153
+ )}
154
+ <div className="lp-popover-actions-end">
155
+ <button
156
+ type="button"
157
+ onClick={onClose}
158
+ className="lp-popover-btn lp-popover-btn--secondary"
159
+ >
160
+ {cancelLabel}
161
+ </button>
162
+ <button
163
+ type="submit"
164
+ disabled={!url.trim()}
165
+ className="lp-popover-btn lp-popover-btn--primary"
166
+ >
167
+ {submitLabel}
168
+ </button>
169
+ </div>
170
+ </div>
171
+ );
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Content — form content without Popover wrapper
176
+ // ---------------------------------------------------------------------------
177
+
178
+ export interface LinkPopoverContentProps {
179
+ initialLabel?: string;
180
+ initialUrl?: string;
181
+ onSubmit: (label: string, url: string) => void;
182
+ onRemove: () => void;
183
+ onClose: () => void;
184
+ children: ReactNode;
185
+ }
186
+
187
+ function LinkPopoverContent({
188
+ initialLabel,
189
+ initialUrl,
190
+ onSubmit,
191
+ onRemove,
192
+ onClose,
193
+ children,
194
+ }: LinkPopoverContentProps): JSX.Element {
195
+ const [label, setLabel] = useState(initialLabel || "");
196
+ const [url, setUrl] = useState(initialUrl || "");
197
+ const isEditing = initialUrl !== "";
198
+
199
+ const handleSubmit = useCallback(
200
+ (e: React.SubmitEvent) => {
201
+ e.preventDefault();
202
+ if (!url.trim()) return;
203
+ onSubmit(label || url, url);
204
+ },
205
+ [label, url, onSubmit],
206
+ );
207
+
208
+ const ctxValue = useMemo(
209
+ () => ({ label, setLabel, url, setUrl, isEditing, onRemove, onClose }),
210
+ [label, url, isEditing, onRemove, onClose],
211
+ );
212
+
213
+ return (
214
+ <LinkPopoverCtx value={ctxValue}>
215
+ <Form onSubmit={handleSubmit} className="lp-popover-form">
216
+ {children}
217
+ </Form>
218
+ </LinkPopoverCtx>
219
+ );
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Root — standalone with its own Popover wrapper
224
+ // ---------------------------------------------------------------------------
225
+
226
+ export interface LinkPopoverProps extends LinkPopoverContentProps {
227
+ x: number;
228
+ y: number;
229
+ }
230
+
231
+ function LinkPopoverRoot({ x, y, onClose, ...contentProps }: LinkPopoverProps): JSX.Element {
232
+ const virtualAnchor = useMemo(
233
+ () => ({
234
+ getBoundingClientRect: () => ({
235
+ x,
236
+ y,
237
+ width: 0,
238
+ height: 0,
239
+ top: y,
240
+ left: x,
241
+ right: x,
242
+ bottom: y,
243
+ toJSON() {
244
+ return this;
245
+ },
246
+ }),
247
+ }),
248
+ [x, y],
249
+ );
250
+
251
+ return (
252
+ <Popover.Root
253
+ open
254
+ onOpenChange={(open) => {
255
+ if (!open) onClose();
256
+ }}
257
+ >
258
+ <Popover.Portal>
259
+ <Popover.Positioner
260
+ anchor={virtualAnchor}
261
+ positionMethod="fixed"
262
+ side="bottom"
263
+ align="start"
264
+ sideOffset={8}
265
+ className="lp-popover-positioner"
266
+ >
267
+ <Popover.Popup className="lp-popover">
268
+ <LinkPopoverContent onClose={onClose} {...contentProps} />
269
+ </Popover.Popup>
270
+ </Popover.Positioner>
271
+ </Popover.Portal>
272
+ </Popover.Root>
273
+ );
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Connected — subscribes to StateField, owns its own Popover.Root
278
+ // ---------------------------------------------------------------------------
279
+
280
+ export function LinkPopoverConnected({ children }: { children: ReactNode }): JSX.Element {
281
+ const { view } = useEditorContext();
282
+ const popoverReq = useStateField(popoverField, null);
283
+ const linkReq = popoverReq?.type === "link" ? (popoverReq as LinkRequest) : null;
284
+
285
+ const handleSubmit = useCallback(
286
+ (label: string, url: string) => {
287
+ if (!view || !linkReq) return;
288
+ const { from, to } = linkReq.data;
289
+ insertLink(view, from, to, label, url);
290
+ view.dispatch({ effects: closePopover.of() });
291
+ },
292
+ [view, linkReq],
293
+ );
294
+
295
+ const handleRemove = useCallback(() => {
296
+ if (!view || !linkReq) return;
297
+ const { from, to, text } = linkReq.data;
298
+ view.dispatch({
299
+ changes: { from, to, insert: text },
300
+ selection: EditorSelection.cursor(from + text.length),
301
+ });
302
+ view.dispatch({ effects: closePopover.of() });
303
+ view.focus();
304
+ }, [view, linkReq]);
305
+
306
+ const handleClose = useCallback(() => {
307
+ if (!view) return;
308
+ view.dispatch({ effects: closePopover.of() });
309
+ view.focus();
310
+ }, [view]);
311
+
312
+ const virtualAnchor = useVirtualAnchor(linkReq?.data.from ?? null);
313
+
314
+ return (
315
+ <Popover.Root
316
+ open={!!linkReq}
317
+ onOpenChange={(open) => {
318
+ if (!open) handleClose();
319
+ }}
320
+ >
321
+ <Popover.Portal>
322
+ <Popover.Positioner
323
+ anchor={virtualAnchor}
324
+ positionMethod="fixed"
325
+ side="bottom"
326
+ align="start"
327
+ sideOffset={8}
328
+ className="lp-popover-positioner"
329
+ >
330
+ <Popover.Popup className="lp-popover">
331
+ {linkReq && (
332
+ <LinkPopoverContent
333
+ key={`${linkReq.data.from}-${linkReq.data.to}`}
334
+ initialLabel={linkReq.data.text}
335
+ initialUrl={linkReq.data.url}
336
+ onSubmit={handleSubmit}
337
+ onRemove={handleRemove}
338
+ onClose={handleClose}
339
+ >
340
+ {children}
341
+ </LinkPopoverContent>
342
+ )}
343
+ </Popover.Popup>
344
+ </Popover.Positioner>
345
+ </Popover.Portal>
346
+ </Popover.Root>
347
+ );
348
+ }
349
+
350
+ // ---------------------------------------------------------------------------
351
+ // Namespace exports
352
+ // ---------------------------------------------------------------------------
353
+
354
+ export { LinkPopoverRoot as Root, LinkPopoverContent as Content, LabelInput, UrlInput, Actions };
@@ -0,0 +1,80 @@
1
+ import { useEffect, useState, type JSX } from "react";
2
+ import { Marked } from "marked";
3
+ import { markedHighlight } from "marked-highlight";
4
+ import { highlightCode } from "../core/highlight";
5
+
6
+ function parseFenceInfo(info: string): { lang: string; filename: string } {
7
+ const parts = info.trim().split(/\s+/);
8
+ let lang = "";
9
+ let filename = "";
10
+ for (const part of parts) {
11
+ if (part.startsWith("filename=")) {
12
+ filename = part.slice("filename=".length);
13
+ } else if (!lang) {
14
+ lang = part;
15
+ }
16
+ }
17
+ return { lang, filename };
18
+ }
19
+
20
+ const renderer = {
21
+ code({ text, lang: rawLang }: { text: string; lang?: string }) {
22
+ const { lang, filename } = parseFenceInfo(rawLang || "");
23
+ let header = "";
24
+ if (filename || lang) {
25
+ const left = filename || lang;
26
+ const right = filename && lang ? `<span class="lp-code-header-lang">${lang}</span>` : "";
27
+ header = `<div class="lp-code-header"><span>${left}</span>${right}</div>`;
28
+ }
29
+ const lines = text.split("\n");
30
+ if (lines.at(-1) === "") lines.pop();
31
+ const rows = lines
32
+ .map(
33
+ (line, i) =>
34
+ `<span class="lp-code-row"><span class="lp-code-line-number">${i + 1}</span><span class="lp-code-line">${line || " "}</span></span>`,
35
+ )
36
+ .join("");
37
+ return `<div class="lp-code-block">${header}<pre><code class="language-${lang}">${rows}</code></pre></div>`;
38
+ },
39
+ };
40
+
41
+ const marked = new Marked(
42
+ { gfm: true },
43
+ markedHighlight({
44
+ async: true,
45
+ langPrefix: "language-",
46
+ async highlight(code, lang) {
47
+ const actualLang = lang.split(/\s+/)[0];
48
+ if (!actualLang) return code;
49
+ return highlightCode(code, actualLang);
50
+ },
51
+ }),
52
+ { renderer },
53
+ );
54
+
55
+ export interface PreviewProps {
56
+ value: string;
57
+ className?: string;
58
+ }
59
+
60
+ export function Preview({ value, className }: PreviewProps): JSX.Element {
61
+ const [html, setHtml] = useState("");
62
+
63
+ useEffect(() => {
64
+ if (!value) {
65
+ setHtml("");
66
+ return;
67
+ }
68
+ let cancelled = false;
69
+ (marked.parse(value) as Promise<string>).then((result) => {
70
+ if (!cancelled) setHtml(result);
71
+ });
72
+ return () => {
73
+ cancelled = true;
74
+ };
75
+ }, [value]);
76
+
77
+ return (
78
+ <div className={`lp-preview ${className || ""}`} dangerouslySetInnerHTML={{ __html: html }} />
79
+ );
80
+ }