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.
- package/dist/core/active-marks.d.ts +15 -0
- package/dist/core/active-marks.js +57 -0
- package/dist/core/commands.d.ts +39 -0
- package/dist/core/commands.js +415 -0
- package/dist/core/editor.d.ts +25 -0
- package/dist/core/editor.js +102 -0
- package/dist/core/field-notifier.d.ts +21 -0
- package/dist/core/field-notifier.js +56 -0
- package/dist/core/highlight-style.js +60 -0
- package/dist/core/highlight.d.ts +8 -0
- package/dist/core/highlight.js +34 -0
- package/dist/core/plugins/blockquote.d.ts +11 -0
- package/dist/core/plugins/blockquote.js +78 -0
- package/dist/core/plugins/bracket.d.ts +6 -0
- package/dist/core/plugins/bracket.js +38 -0
- package/dist/core/plugins/code-block.d.ts +27 -0
- package/dist/core/plugins/code-block.js +207 -0
- package/dist/core/plugins/heading.d.ts +13 -0
- package/dist/core/plugins/heading.js +111 -0
- package/dist/core/plugins/inline.d.ts +14 -0
- package/dist/core/plugins/inline.js +103 -0
- package/dist/core/plugins/link.d.ts +25 -0
- package/dist/core/plugins/link.js +104 -0
- package/dist/core/plugins/list.d.ts +14 -0
- package/dist/core/plugins/list.js +91 -0
- package/dist/core/plugins/table.d.ts +12 -0
- package/dist/core/plugins/table.js +161 -0
- package/dist/core/plugins.d.ts +9 -0
- package/dist/core/plugins.js +9 -0
- package/dist/core/popover.d.ts +9 -0
- package/dist/core/popover.js +16 -0
- package/dist/core/registry.d.ts +10 -0
- package/dist/core/registry.js +8 -0
- package/dist/core/types.d.ts +25 -0
- package/dist/core/types.js +0 -0
- package/dist/core/utils.d.ts +13 -0
- package/dist/core/utils.js +32 -0
- package/dist/leepi.css +461 -0
- package/dist/react/code-block-popover.d.ts +76 -0
- package/dist/react/code-block-popover.js +223 -0
- package/dist/react/context.d.ts +42 -0
- package/dist/react/context.js +88 -0
- package/dist/react/editor.d.ts +30 -0
- package/dist/react/editor.js +60 -0
- package/dist/react/floating-toolbar.d.ts +30 -0
- package/dist/react/floating-toolbar.js +87 -0
- package/dist/react/link-popover.d.ts +70 -0
- package/dist/react/link-popover.js +222 -0
- package/dist/react/preview.d.ts +13 -0
- package/dist/react/preview.js +56 -0
- package/dist/react/toolbar.d.ts +51 -0
- package/dist/react/toolbar.js +161 -0
- package/package.json +90 -1
- package/src/core/active-marks.ts +89 -0
- package/src/core/commands.ts +461 -0
- package/src/core/editor.ts +139 -0
- package/src/core/field-notifier.ts +71 -0
- package/src/core/highlight-style.ts +66 -0
- package/src/core/highlight.ts +50 -0
- package/src/core/plugins/blockquote.ts +108 -0
- package/src/core/plugins/bracket.ts +34 -0
- package/src/core/plugins/code-block.ts +195 -0
- package/src/core/plugins/heading.ts +95 -0
- package/src/core/plugins/index.ts +16 -0
- package/src/core/plugins/inline.ts +62 -0
- package/src/core/plugins/link.ts +124 -0
- package/src/core/plugins/list.ts +68 -0
- package/src/core/plugins/table.ts +217 -0
- package/src/core/popover.ts +17 -0
- package/src/core/registry.ts +18 -0
- package/src/core/types.ts +25 -0
- package/src/core/utils.ts +38 -0
- package/src/react/code-block-popover.tsx +387 -0
- package/src/react/context.tsx +153 -0
- package/src/react/editor.tsx +106 -0
- package/src/react/floating-toolbar.tsx +161 -0
- package/src/react/link-popover.tsx +354 -0
- package/src/react/preview.tsx +80 -0
- package/src/react/toolbar.tsx +294 -0
- package/src/styles/floating-toolbar.css +52 -0
- package/src/styles/leepi.css +2 -0
- package/src/styles/popover.css +93 -0
- package/src/styles/preview.css +191 -0
- package/src/styles/theme.css +99 -0
- package/src/styles/tokens.css +63 -0
- package/src/styles/toolbar.css +55 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { applyCodeBlock } from "../core/commands.js";
|
|
2
|
+
import { closePopover, popoverField } from "../core/popover.js";
|
|
3
|
+
import { useEditorContext, useStateField, useVirtualAnchor } from "./context.js";
|
|
4
|
+
import { createContext, useCallback, useContext, useId, useMemo, useState } from "react";
|
|
5
|
+
import { Popover } from "@base-ui/react/popover";
|
|
6
|
+
import { Form } from "@base-ui/react/form";
|
|
7
|
+
import { Field } from "@base-ui/react/field";
|
|
8
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
9
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
//#region src/react/code-block-popover.tsx
|
|
12
|
+
const CodeBlockPopoverCtx = createContext(null);
|
|
13
|
+
function useCodeBlockPopoverCtx() {
|
|
14
|
+
const ctx = useContext(CodeBlockPopoverCtx);
|
|
15
|
+
if (!ctx) throw new Error("leepi: CodeBlockPopover sub-components must be used inside CodeBlockPopover.Root");
|
|
16
|
+
return ctx;
|
|
17
|
+
}
|
|
18
|
+
function LanguageSelect({ autoFocus, render, label, children, ...restProps }) {
|
|
19
|
+
const { lang, setLang } = useCodeBlockPopoverCtx();
|
|
20
|
+
const id = useId();
|
|
21
|
+
const element = useRender({
|
|
22
|
+
render,
|
|
23
|
+
defaultTagName: "select",
|
|
24
|
+
props: mergeProps({
|
|
25
|
+
id,
|
|
26
|
+
value: lang,
|
|
27
|
+
onChange: (e) => setLang(e.target.value),
|
|
28
|
+
autoFocus,
|
|
29
|
+
className: "lp-popover-select"
|
|
30
|
+
}, restProps)
|
|
31
|
+
});
|
|
32
|
+
const defaultSelect = !render ? /* @__PURE__ */ jsx("select", {
|
|
33
|
+
id,
|
|
34
|
+
value: lang,
|
|
35
|
+
onChange: (e) => setLang(e.target.value),
|
|
36
|
+
autoFocus,
|
|
37
|
+
className: "lp-popover-select",
|
|
38
|
+
...restProps,
|
|
39
|
+
children
|
|
40
|
+
}) : null;
|
|
41
|
+
return /* @__PURE__ */ jsxs(Field.Root, {
|
|
42
|
+
className: "lp-popover-field",
|
|
43
|
+
children: [/* @__PURE__ */ jsx(Field.Label, {
|
|
44
|
+
htmlFor: id,
|
|
45
|
+
className: "lp-popover-label",
|
|
46
|
+
children: label
|
|
47
|
+
}), render ? element : defaultSelect]
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function LanguageInput({ autoFocus, placeholder, render, children, ...restProps }) {
|
|
51
|
+
const { lang, setLang } = useCodeBlockPopoverCtx();
|
|
52
|
+
const id = useId();
|
|
53
|
+
const element = useRender({
|
|
54
|
+
render,
|
|
55
|
+
defaultTagName: "input",
|
|
56
|
+
props: mergeProps({
|
|
57
|
+
id,
|
|
58
|
+
type: "text",
|
|
59
|
+
value: lang,
|
|
60
|
+
onChange: (e) => setLang(e.target.value),
|
|
61
|
+
autoFocus,
|
|
62
|
+
placeholder,
|
|
63
|
+
className: "lp-popover-input"
|
|
64
|
+
}, restProps)
|
|
65
|
+
});
|
|
66
|
+
return /* @__PURE__ */ jsxs(Field.Root, {
|
|
67
|
+
className: "lp-popover-field",
|
|
68
|
+
children: [/* @__PURE__ */ jsx(Field.Label, {
|
|
69
|
+
htmlFor: id,
|
|
70
|
+
className: "lp-popover-label",
|
|
71
|
+
children
|
|
72
|
+
}), element]
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function FilenameInput({ placeholder, render, children, ...restProps }) {
|
|
76
|
+
const { filename, setFilename } = useCodeBlockPopoverCtx();
|
|
77
|
+
const id = useId();
|
|
78
|
+
const element = useRender({
|
|
79
|
+
render,
|
|
80
|
+
defaultTagName: "input",
|
|
81
|
+
props: mergeProps({
|
|
82
|
+
id,
|
|
83
|
+
type: "text",
|
|
84
|
+
value: filename,
|
|
85
|
+
onChange: (e) => setFilename(e.target.value),
|
|
86
|
+
placeholder,
|
|
87
|
+
className: "lp-popover-input"
|
|
88
|
+
}, restProps)
|
|
89
|
+
});
|
|
90
|
+
return /* @__PURE__ */ jsxs(Field.Root, {
|
|
91
|
+
className: "lp-popover-field",
|
|
92
|
+
children: [/* @__PURE__ */ jsx(Field.Label, {
|
|
93
|
+
htmlFor: id,
|
|
94
|
+
className: "lp-popover-label",
|
|
95
|
+
children
|
|
96
|
+
}), element]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function Actions({ cancelLabel, submitLabel }) {
|
|
100
|
+
const { onClose } = useCodeBlockPopoverCtx();
|
|
101
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
102
|
+
className: "lp-popover-actions",
|
|
103
|
+
children: [/* @__PURE__ */ jsx("span", {}), /* @__PURE__ */ jsxs("div", {
|
|
104
|
+
className: "lp-popover-actions-end",
|
|
105
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
106
|
+
type: "button",
|
|
107
|
+
onClick: onClose,
|
|
108
|
+
className: "lp-popover-btn lp-popover-btn--secondary",
|
|
109
|
+
children: cancelLabel
|
|
110
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
111
|
+
type: "submit",
|
|
112
|
+
className: "lp-popover-btn lp-popover-btn--primary",
|
|
113
|
+
children: submitLabel
|
|
114
|
+
})]
|
|
115
|
+
})]
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function CodeBlockPopoverContent({ initialLang, initialFilename, onSubmit, onClose, children }) {
|
|
119
|
+
const [lang, setLang] = useState(initialLang || "");
|
|
120
|
+
const [filename, setFilename] = useState(initialFilename || "");
|
|
121
|
+
const handleSubmit = useCallback((e) => {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
onSubmit(lang, filename);
|
|
124
|
+
}, [
|
|
125
|
+
lang,
|
|
126
|
+
filename,
|
|
127
|
+
onSubmit
|
|
128
|
+
]);
|
|
129
|
+
return /* @__PURE__ */ jsx(CodeBlockPopoverCtx, {
|
|
130
|
+
value: useMemo(() => ({
|
|
131
|
+
lang,
|
|
132
|
+
setLang,
|
|
133
|
+
filename,
|
|
134
|
+
setFilename,
|
|
135
|
+
onClose
|
|
136
|
+
}), [
|
|
137
|
+
lang,
|
|
138
|
+
filename,
|
|
139
|
+
onClose
|
|
140
|
+
]),
|
|
141
|
+
children: /* @__PURE__ */ jsx(Form, {
|
|
142
|
+
onSubmit: handleSubmit,
|
|
143
|
+
className: "lp-popover-form",
|
|
144
|
+
children
|
|
145
|
+
})
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function CodeBlockPopoverRoot({ x, y, onClose, ...contentProps }) {
|
|
149
|
+
const virtualAnchor = useMemo(() => ({ getBoundingClientRect: () => ({
|
|
150
|
+
x,
|
|
151
|
+
y,
|
|
152
|
+
width: 0,
|
|
153
|
+
height: 0,
|
|
154
|
+
top: y,
|
|
155
|
+
left: x,
|
|
156
|
+
right: x,
|
|
157
|
+
bottom: y,
|
|
158
|
+
toJSON() {
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
}) }), [x, y]);
|
|
162
|
+
return /* @__PURE__ */ jsx(Popover.Root, {
|
|
163
|
+
open: true,
|
|
164
|
+
onOpenChange: onClose,
|
|
165
|
+
children: /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
|
|
166
|
+
anchor: virtualAnchor,
|
|
167
|
+
positionMethod: "fixed",
|
|
168
|
+
side: "bottom",
|
|
169
|
+
align: "start",
|
|
170
|
+
sideOffset: 8,
|
|
171
|
+
className: "lp-popover-positioner",
|
|
172
|
+
children: /* @__PURE__ */ jsx(Popover.Popup, {
|
|
173
|
+
className: "lp-popover",
|
|
174
|
+
children: /* @__PURE__ */ jsx(CodeBlockPopoverContent, {
|
|
175
|
+
onClose,
|
|
176
|
+
...contentProps
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
}) })
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function CodeBlockPopoverConnected({ children }) {
|
|
183
|
+
const { view } = useEditorContext();
|
|
184
|
+
const popoverReq = useStateField(popoverField, null);
|
|
185
|
+
const codeBlockReq = popoverReq?.type === "codeblock" ? popoverReq : null;
|
|
186
|
+
const handleSubmit = useCallback((lang, filename) => {
|
|
187
|
+
if (!view || !codeBlockReq) return;
|
|
188
|
+
applyCodeBlock(view, codeBlockReq.data, lang, filename);
|
|
189
|
+
view.dispatch({ effects: closePopover.of() });
|
|
190
|
+
}, [view, codeBlockReq]);
|
|
191
|
+
const handleClose = useCallback(() => {
|
|
192
|
+
if (!view) return;
|
|
193
|
+
view.dispatch({ effects: closePopover.of() });
|
|
194
|
+
view.focus();
|
|
195
|
+
}, [view]);
|
|
196
|
+
const virtualAnchor = useVirtualAnchor(codeBlockReq ? codeBlockReq.data.isNew ? codeBlockReq.data.insertPos ?? 0 : codeBlockReq.data.fenceFrom : null);
|
|
197
|
+
return /* @__PURE__ */ jsx(Popover.Root, {
|
|
198
|
+
open: !!codeBlockReq,
|
|
199
|
+
onOpenChange: (open) => {
|
|
200
|
+
if (!open) handleClose();
|
|
201
|
+
},
|
|
202
|
+
children: /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
|
|
203
|
+
anchor: virtualAnchor,
|
|
204
|
+
positionMethod: "fixed",
|
|
205
|
+
side: "bottom",
|
|
206
|
+
align: "start",
|
|
207
|
+
sideOffset: 8,
|
|
208
|
+
className: "lp-popover-positioner",
|
|
209
|
+
children: /* @__PURE__ */ jsx(Popover.Popup, {
|
|
210
|
+
className: "lp-popover",
|
|
211
|
+
children: codeBlockReq && /* @__PURE__ */ jsx(CodeBlockPopoverContent, {
|
|
212
|
+
initialLang: codeBlockReq.data.lang,
|
|
213
|
+
initialFilename: codeBlockReq.data.filename,
|
|
214
|
+
onSubmit: handleSubmit,
|
|
215
|
+
onClose: handleClose,
|
|
216
|
+
children
|
|
217
|
+
}, `${codeBlockReq.data.fenceFrom}-${codeBlockReq.data.isNew}`)
|
|
218
|
+
})
|
|
219
|
+
}) })
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
export { Actions, CodeBlockPopoverConnected, CodeBlockPopoverContent as Content, FilenameInput, LanguageInput, LanguageSelect, CodeBlockPopoverRoot as Root };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ActiveMarks } from "../core/active-marks.js";
|
|
2
|
+
import { ShortcutEntry } from "../core/types.js";
|
|
3
|
+
import { EditorView } from "@codemirror/view";
|
|
4
|
+
import { StateField } from "@codemirror/state";
|
|
5
|
+
import { JSX, ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
//#region src/react/context.d.ts
|
|
8
|
+
interface EditorContextValue {
|
|
9
|
+
view: EditorView | null;
|
|
10
|
+
setView: (view: EditorView | null) => void;
|
|
11
|
+
}
|
|
12
|
+
declare function useEditorContext(): EditorContextValue;
|
|
13
|
+
interface EditorProviderProps {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
declare function EditorProvider({
|
|
17
|
+
children
|
|
18
|
+
}: EditorProviderProps): JSX.Element;
|
|
19
|
+
/**
|
|
20
|
+
* Returns the currently active formatting marks at the cursor position.
|
|
21
|
+
* Updates reactively when the cursor moves or the document changes.
|
|
22
|
+
*/
|
|
23
|
+
declare function useActiveMarks(): ActiveMarks;
|
|
24
|
+
/**
|
|
25
|
+
* Subscribe to a CM6 StateField and re-render when its value changes.
|
|
26
|
+
*/
|
|
27
|
+
declare function useStateField<T>(field: StateField<T>, defaultValue: T): T;
|
|
28
|
+
/**
|
|
29
|
+
* Returns a virtual anchor element that dynamically resolves its position
|
|
30
|
+
* from a CodeMirror document position. Useful for anchoring popovers to
|
|
31
|
+
* editor content that may scroll after the popover opens.
|
|
32
|
+
*/
|
|
33
|
+
interface VirtualAnchor {
|
|
34
|
+
getBoundingClientRect: () => DOMRect;
|
|
35
|
+
}
|
|
36
|
+
declare function useVirtualAnchor(pos: number | null): VirtualAnchor | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Returns all shortcuts registered by plugins via the shortcutRegistry Facet.
|
|
39
|
+
*/
|
|
40
|
+
declare function useShortcuts(): ShortcutEntry[];
|
|
41
|
+
//#endregion
|
|
42
|
+
export { EditorContextValue, EditorProvider, EditorProviderProps, VirtualAnchor, useActiveMarks, useEditorContext, useShortcuts, useStateField, useVirtualAnchor };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { shortcutRegistry } from "../core/registry.js";
|
|
2
|
+
import { emptyMarks, getMarksSnapshot, subscribeToMarks } from "../core/active-marks.js";
|
|
3
|
+
import { getFieldSnapshot, subscribeToField } from "../core/field-notifier.js";
|
|
4
|
+
import { createContext, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
//#region src/react/context.tsx
|
|
7
|
+
const EditorContext = createContext(null);
|
|
8
|
+
function useEditorContext() {
|
|
9
|
+
const ctx = useContext(EditorContext);
|
|
10
|
+
if (!ctx) throw new Error("leepi: <Toolbar />, <FloatingToolbar /> must be rendered inside <EditorProvider>");
|
|
11
|
+
return ctx;
|
|
12
|
+
}
|
|
13
|
+
function EditorProvider({ children }) {
|
|
14
|
+
const [view, setView] = useState(null);
|
|
15
|
+
const editorCtx = useMemo(() => ({
|
|
16
|
+
view,
|
|
17
|
+
setView
|
|
18
|
+
}), [view]);
|
|
19
|
+
return /* @__PURE__ */ jsx(EditorContext.Provider, {
|
|
20
|
+
value: editorCtx,
|
|
21
|
+
children
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Returns the currently active formatting marks at the cursor position.
|
|
26
|
+
* Updates reactively when the cursor moves or the document changes.
|
|
27
|
+
*/
|
|
28
|
+
function useActiveMarks() {
|
|
29
|
+
const { view } = useEditorContext();
|
|
30
|
+
return useSyncExternalStore(useCallback((onStoreChange) => {
|
|
31
|
+
if (!view) return () => {};
|
|
32
|
+
return subscribeToMarks(view, onStoreChange);
|
|
33
|
+
}, [view]), useCallback(() => {
|
|
34
|
+
if (!view) return emptyMarks;
|
|
35
|
+
return getMarksSnapshot(view);
|
|
36
|
+
}, [view]));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Subscribe to a CM6 StateField and re-render when its value changes.
|
|
40
|
+
*/
|
|
41
|
+
function useStateField(field, defaultValue) {
|
|
42
|
+
const { view } = useEditorContext();
|
|
43
|
+
return useSyncExternalStore(useCallback((onStoreChange) => {
|
|
44
|
+
if (!view) return () => {};
|
|
45
|
+
return subscribeToField(view, field, onStoreChange);
|
|
46
|
+
}, [view, field]), useCallback(() => {
|
|
47
|
+
if (!view) return defaultValue;
|
|
48
|
+
return getFieldSnapshot(view, field) ?? defaultValue;
|
|
49
|
+
}, [
|
|
50
|
+
view,
|
|
51
|
+
field,
|
|
52
|
+
defaultValue
|
|
53
|
+
]));
|
|
54
|
+
}
|
|
55
|
+
function useVirtualAnchor(pos) {
|
|
56
|
+
const { view } = useEditorContext();
|
|
57
|
+
return useMemo(() => {
|
|
58
|
+
if (pos == null || !view) return void 0;
|
|
59
|
+
return { getBoundingClientRect: () => {
|
|
60
|
+
const coords = view.coordsAtPos(pos);
|
|
61
|
+
const x = coords?.left ?? 0;
|
|
62
|
+
const y = coords?.bottom ?? 0;
|
|
63
|
+
return {
|
|
64
|
+
x,
|
|
65
|
+
y,
|
|
66
|
+
width: 0,
|
|
67
|
+
height: 0,
|
|
68
|
+
top: y,
|
|
69
|
+
left: x,
|
|
70
|
+
right: x,
|
|
71
|
+
bottom: y,
|
|
72
|
+
toJSON() {
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
} };
|
|
77
|
+
}, [pos, view]);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Returns all shortcuts registered by plugins via the shortcutRegistry Facet.
|
|
81
|
+
*/
|
|
82
|
+
function useShortcuts() {
|
|
83
|
+
const { view } = useEditorContext();
|
|
84
|
+
if (!view) return [];
|
|
85
|
+
return view.state.facet(shortcutRegistry);
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
export { EditorProvider, useActiveMarks, useEditorContext, useShortcuts, useStateField, useVirtualAnchor };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Extension } from "@codemirror/state";
|
|
2
|
+
import { ComponentProps, JSX, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/react/editor.d.ts
|
|
5
|
+
type EditorProps = {
|
|
6
|
+
onChange?: (value: string) => void;
|
|
7
|
+
placeholder?: string; /** Additional CM6 extensions / plugins */
|
|
8
|
+
plugins?: Extension[];
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
} & Pick<ComponentProps<"textarea">, "className" | "style" | "readOnly" | "autoFocus"> & ({
|
|
11
|
+
value: string;
|
|
12
|
+
defaultValue?: never;
|
|
13
|
+
} | {
|
|
14
|
+
value?: never;
|
|
15
|
+
defaultValue: string;
|
|
16
|
+
});
|
|
17
|
+
declare function EditorRoot({
|
|
18
|
+
value,
|
|
19
|
+
defaultValue,
|
|
20
|
+
onChange,
|
|
21
|
+
placeholder,
|
|
22
|
+
plugins,
|
|
23
|
+
readOnly,
|
|
24
|
+
autoFocus,
|
|
25
|
+
children,
|
|
26
|
+
className,
|
|
27
|
+
style
|
|
28
|
+
}: EditorProps): JSX.Element;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { EditorRoot as Editor, EditorProps };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createEditorState, editableCompartment, loadLanguageSupport } from "../core/editor.js";
|
|
2
|
+
import { useEditorContext } from "./context.js";
|
|
3
|
+
import { EditorView } from "@codemirror/view";
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/react/editor.tsx
|
|
7
|
+
function EditorRoot({ value, defaultValue, onChange, placeholder = "", plugins, readOnly = false, autoFocus = false, children, className, style }) {
|
|
8
|
+
const isControlled = value !== void 0;
|
|
9
|
+
const containerRef = useRef(null);
|
|
10
|
+
const viewRef = useRef(null);
|
|
11
|
+
const onChangeRef = useRef(onChange);
|
|
12
|
+
onChangeRef.current = onChange;
|
|
13
|
+
const { setView } = useEditorContext();
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!containerRef.current) return;
|
|
16
|
+
containerRef.current.style.display = "contents";
|
|
17
|
+
const view = new EditorView({
|
|
18
|
+
state: createEditorState({
|
|
19
|
+
doc: value ?? defaultValue ?? "",
|
|
20
|
+
onUpdate: (doc) => onChangeRef.current?.(doc),
|
|
21
|
+
plugins,
|
|
22
|
+
placeholder,
|
|
23
|
+
readOnly
|
|
24
|
+
}),
|
|
25
|
+
parent: containerRef.current
|
|
26
|
+
});
|
|
27
|
+
viewRef.current = view;
|
|
28
|
+
setView(view);
|
|
29
|
+
if (autoFocus) view.focus();
|
|
30
|
+
loadLanguageSupport(view);
|
|
31
|
+
return () => {
|
|
32
|
+
view.destroy();
|
|
33
|
+
viewRef.current = null;
|
|
34
|
+
setView(null);
|
|
35
|
+
};
|
|
36
|
+
}, [setView]);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!isControlled) return;
|
|
39
|
+
const view = viewRef.current;
|
|
40
|
+
if (!view) return;
|
|
41
|
+
const currentDoc = view.state.doc.toString();
|
|
42
|
+
if (currentDoc !== value) view.dispatch({ changes: {
|
|
43
|
+
from: 0,
|
|
44
|
+
to: currentDoc.length,
|
|
45
|
+
insert: value ?? ""
|
|
46
|
+
} });
|
|
47
|
+
}, [isControlled, value]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const view = viewRef.current;
|
|
50
|
+
if (!view) return;
|
|
51
|
+
view.dispatch({ effects: editableCompartment.reconfigure(EditorView.editable.of(!readOnly)) });
|
|
52
|
+
}, [readOnly]);
|
|
53
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
54
|
+
ref: containerRef,
|
|
55
|
+
className,
|
|
56
|
+
style
|
|
57
|
+
}), children] });
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { EditorRoot as Editor };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ComponentProps, JSX, ReactNode } from "react";
|
|
2
|
+
import { Toolbar } from "@base-ui/react/toolbar";
|
|
3
|
+
|
|
4
|
+
//#region src/react/floating-toolbar.d.ts
|
|
5
|
+
type FloatingToolbarButtonProps = ComponentProps<typeof Toolbar.Button>;
|
|
6
|
+
declare function FloatingToolbarButton({
|
|
7
|
+
className,
|
|
8
|
+
...props
|
|
9
|
+
}: FloatingToolbarButtonProps): JSX.Element;
|
|
10
|
+
type FloatingToolbarSeparatorProps = ComponentProps<typeof Toolbar.Separator>;
|
|
11
|
+
declare function FloatingToolbarSeparator({
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: FloatingToolbarSeparatorProps): JSX.Element;
|
|
15
|
+
type FloatingToolbarGroupProps = ComponentProps<typeof Toolbar.Group>;
|
|
16
|
+
declare function FloatingToolbarGroup({
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: FloatingToolbarGroupProps): JSX.Element;
|
|
20
|
+
type FloatingToolbarProps = {
|
|
21
|
+
className?: string;
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
} & Omit<ComponentProps<typeof Toolbar.Root>, "children">;
|
|
24
|
+
declare function FloatingToolbarRoot({
|
|
25
|
+
className,
|
|
26
|
+
children,
|
|
27
|
+
style: propStyle
|
|
28
|
+
}: FloatingToolbarProps): JSX.Element | null;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { FloatingToolbarButton as Button, FloatingToolbarProps, FloatingToolbarGroup as Group, FloatingToolbarRoot as Root, FloatingToolbarSeparator as Separator };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isInsideCodeBlock } from "../core/utils.js";
|
|
2
|
+
import { popoverField } from "../core/popover.js";
|
|
3
|
+
import { useEditorContext, useStateField } from "./context.js";
|
|
4
|
+
import { useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { Toolbar } from "@base-ui/react/toolbar";
|
|
7
|
+
//#region src/react/floating-toolbar.tsx
|
|
8
|
+
function useFloatingPosition() {
|
|
9
|
+
const { view } = useEditorContext();
|
|
10
|
+
const popoverOpen = useStateField(popoverField, null) != null;
|
|
11
|
+
const [position, setPosition] = useState(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!view) return;
|
|
14
|
+
const updatePosition = () => {
|
|
15
|
+
const { state } = view;
|
|
16
|
+
const { from, to } = state.selection.main;
|
|
17
|
+
if (from === to || isInsideCodeBlock(view)) {
|
|
18
|
+
setPosition(null);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (state.doc.lineAt(from).number !== state.doc.lineAt(to).number) {
|
|
22
|
+
setPosition(null);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const fromCoords = view.coordsAtPos(from);
|
|
26
|
+
const toCoords = view.coordsAtPos(to);
|
|
27
|
+
if (!fromCoords || !toCoords) {
|
|
28
|
+
setPosition(null);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const left = (fromCoords.left + toCoords.left) / 2;
|
|
32
|
+
setPosition({
|
|
33
|
+
top: Math.min(fromCoords.top, toCoords.top) - 8,
|
|
34
|
+
left
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const handler = () => requestAnimationFrame(updatePosition);
|
|
38
|
+
view.contentDOM.addEventListener("mouseup", handler);
|
|
39
|
+
view.contentDOM.addEventListener("keyup", handler);
|
|
40
|
+
if (!popoverOpen) updatePosition();
|
|
41
|
+
return () => {
|
|
42
|
+
view.contentDOM.removeEventListener("mouseup", handler);
|
|
43
|
+
view.contentDOM.removeEventListener("keyup", handler);
|
|
44
|
+
};
|
|
45
|
+
}, [view, popoverOpen]);
|
|
46
|
+
if (popoverOpen) return null;
|
|
47
|
+
return position;
|
|
48
|
+
}
|
|
49
|
+
function FloatingToolbarButton({ className, ...props }) {
|
|
50
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
51
|
+
className: `lp-floating-toolbar-btn ${className || ""}`,
|
|
52
|
+
...props
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function FloatingToolbarSeparator({ className, ...props }) {
|
|
56
|
+
return /* @__PURE__ */ jsx(Toolbar.Separator, {
|
|
57
|
+
className: `lp-floating-toolbar-separator ${className || ""}`,
|
|
58
|
+
...props
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function FloatingToolbarGroup({ className, ...props }) {
|
|
62
|
+
return /* @__PURE__ */ jsx(Toolbar.Group, {
|
|
63
|
+
className: `lp-floating-toolbar-group ${className || ""}`,
|
|
64
|
+
...props
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function FloatingToolbarRoot({ className, children, style: propStyle }) {
|
|
68
|
+
const position = useFloatingPosition();
|
|
69
|
+
const style = useMemo(() => {
|
|
70
|
+
if (!position) return propStyle;
|
|
71
|
+
return {
|
|
72
|
+
...propStyle,
|
|
73
|
+
position: "fixed",
|
|
74
|
+
top: position.top,
|
|
75
|
+
left: position.left,
|
|
76
|
+
transform: "translate(-50%, -100%)"
|
|
77
|
+
};
|
|
78
|
+
}, [position, propStyle]);
|
|
79
|
+
if (!position) return null;
|
|
80
|
+
return /* @__PURE__ */ jsx(Toolbar.Root, {
|
|
81
|
+
className: `lp-floating-toolbar ${className || ""}`,
|
|
82
|
+
style,
|
|
83
|
+
children
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { FloatingToolbarButton as Button, FloatingToolbarGroup as Group, FloatingToolbarRoot as Root, FloatingToolbarSeparator as Separator };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { JSX, ReactNode } from "react";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
|
|
4
|
+
//#region src/react/link-popover.d.ts
|
|
5
|
+
interface LabelInputProps extends useRender.ComponentProps<"input"> {
|
|
6
|
+
autoFocus?: boolean;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
declare function LabelInput({
|
|
11
|
+
autoFocus,
|
|
12
|
+
placeholder,
|
|
13
|
+
render,
|
|
14
|
+
children,
|
|
15
|
+
...restProps
|
|
16
|
+
}: LabelInputProps): JSX.Element;
|
|
17
|
+
interface UrlInputProps extends useRender.ComponentProps<"input"> {
|
|
18
|
+
autoFocus?: boolean;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
declare function UrlInput({
|
|
23
|
+
autoFocus,
|
|
24
|
+
placeholder,
|
|
25
|
+
render,
|
|
26
|
+
children,
|
|
27
|
+
...restProps
|
|
28
|
+
}: UrlInputProps): JSX.Element;
|
|
29
|
+
declare function Actions({
|
|
30
|
+
cancelLabel,
|
|
31
|
+
submitLabel,
|
|
32
|
+
removeLabel
|
|
33
|
+
}: {
|
|
34
|
+
cancelLabel?: ReactNode;
|
|
35
|
+
submitLabel?: ReactNode;
|
|
36
|
+
removeLabel?: ReactNode;
|
|
37
|
+
}): JSX.Element;
|
|
38
|
+
interface LinkPopoverContentProps {
|
|
39
|
+
initialLabel?: string;
|
|
40
|
+
initialUrl?: string;
|
|
41
|
+
onSubmit: (label: string, url: string) => void;
|
|
42
|
+
onRemove: () => void;
|
|
43
|
+
onClose: () => void;
|
|
44
|
+
children: ReactNode;
|
|
45
|
+
}
|
|
46
|
+
declare function LinkPopoverContent({
|
|
47
|
+
initialLabel,
|
|
48
|
+
initialUrl,
|
|
49
|
+
onSubmit,
|
|
50
|
+
onRemove,
|
|
51
|
+
onClose,
|
|
52
|
+
children
|
|
53
|
+
}: LinkPopoverContentProps): JSX.Element;
|
|
54
|
+
interface LinkPopoverProps extends LinkPopoverContentProps {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
}
|
|
58
|
+
declare function LinkPopoverRoot({
|
|
59
|
+
x,
|
|
60
|
+
y,
|
|
61
|
+
onClose,
|
|
62
|
+
...contentProps
|
|
63
|
+
}: LinkPopoverProps): JSX.Element;
|
|
64
|
+
declare function LinkPopoverConnected({
|
|
65
|
+
children
|
|
66
|
+
}: {
|
|
67
|
+
children: ReactNode;
|
|
68
|
+
}): JSX.Element;
|
|
69
|
+
//#endregion
|
|
70
|
+
export { Actions, LinkPopoverContent as Content, LabelInput, LinkPopoverConnected, LinkPopoverContentProps, LinkPopoverProps, LinkPopoverRoot as Root, UrlInput };
|