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,222 @@
1
+ import { insertLink } from "../core/commands.js";
2
+ import { closePopover, popoverField } from "../core/popover.js";
3
+ import { useEditorContext, useStateField, useVirtualAnchor } from "./context.js";
4
+ import { EditorSelection } from "@codemirror/state";
5
+ import { createContext, useCallback, useContext, useId, useMemo, useState } from "react";
6
+ import { Popover } from "@base-ui/react/popover";
7
+ import { Form } from "@base-ui/react/form";
8
+ import { Field } from "@base-ui/react/field";
9
+ import { useRender } from "@base-ui/react/use-render";
10
+ import { mergeProps } from "@base-ui/react/merge-props";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ //#region src/react/link-popover.tsx
13
+ const LinkPopoverCtx = createContext(null);
14
+ function useLinkPopoverCtx() {
15
+ const ctx = useContext(LinkPopoverCtx);
16
+ if (!ctx) throw new Error("leepi: LinkPopover sub-components must be used inside LinkPopover.Root");
17
+ return ctx;
18
+ }
19
+ function LabelInput({ autoFocus, placeholder, render, children, ...restProps }) {
20
+ const { label, setLabel } = useLinkPopoverCtx();
21
+ const id = useId();
22
+ const element = useRender({
23
+ render,
24
+ defaultTagName: "input",
25
+ props: mergeProps({
26
+ id,
27
+ value: label,
28
+ onChange: (e) => setLabel(e.target.value),
29
+ autoFocus,
30
+ placeholder,
31
+ className: "lp-popover-input"
32
+ }, restProps)
33
+ });
34
+ return /* @__PURE__ */ jsxs(Field.Root, {
35
+ className: "lp-popover-field",
36
+ children: [/* @__PURE__ */ jsx(Field.Label, {
37
+ htmlFor: id,
38
+ className: "lp-popover-label",
39
+ children
40
+ }), element]
41
+ });
42
+ }
43
+ function UrlInput({ autoFocus, placeholder, render, children, ...restProps }) {
44
+ const { url, setUrl } = useLinkPopoverCtx();
45
+ const id = useId();
46
+ const element = useRender({
47
+ render,
48
+ defaultTagName: "input",
49
+ props: mergeProps({
50
+ id,
51
+ type: "url",
52
+ value: url,
53
+ onChange: (e) => setUrl(e.target.value),
54
+ autoFocus,
55
+ placeholder,
56
+ className: "lp-popover-input"
57
+ }, restProps)
58
+ });
59
+ return /* @__PURE__ */ jsxs(Field.Root, {
60
+ className: "lp-popover-field",
61
+ children: [/* @__PURE__ */ jsx(Field.Label, {
62
+ htmlFor: id,
63
+ className: "lp-popover-label",
64
+ children
65
+ }), element]
66
+ });
67
+ }
68
+ function Actions({ cancelLabel, submitLabel, removeLabel }) {
69
+ const { isEditing, url, onRemove, onClose } = useLinkPopoverCtx();
70
+ return /* @__PURE__ */ jsxs("div", {
71
+ className: "lp-popover-actions",
72
+ children: [isEditing ? /* @__PURE__ */ jsx("button", {
73
+ type: "button",
74
+ onClick: onRemove,
75
+ className: "lp-popover-btn lp-popover-btn--danger",
76
+ children: removeLabel
77
+ }) : /* @__PURE__ */ jsx("span", {}), /* @__PURE__ */ jsxs("div", {
78
+ className: "lp-popover-actions-end",
79
+ children: [/* @__PURE__ */ jsx("button", {
80
+ type: "button",
81
+ onClick: onClose,
82
+ className: "lp-popover-btn lp-popover-btn--secondary",
83
+ children: cancelLabel
84
+ }), /* @__PURE__ */ jsx("button", {
85
+ type: "submit",
86
+ disabled: !url.trim(),
87
+ className: "lp-popover-btn lp-popover-btn--primary",
88
+ children: submitLabel
89
+ })]
90
+ })]
91
+ });
92
+ }
93
+ function LinkPopoverContent({ initialLabel, initialUrl, onSubmit, onRemove, onClose, children }) {
94
+ const [label, setLabel] = useState(initialLabel || "");
95
+ const [url, setUrl] = useState(initialUrl || "");
96
+ const isEditing = initialUrl !== "";
97
+ const handleSubmit = useCallback((e) => {
98
+ e.preventDefault();
99
+ if (!url.trim()) return;
100
+ onSubmit(label || url, url);
101
+ }, [
102
+ label,
103
+ url,
104
+ onSubmit
105
+ ]);
106
+ return /* @__PURE__ */ jsx(LinkPopoverCtx, {
107
+ value: useMemo(() => ({
108
+ label,
109
+ setLabel,
110
+ url,
111
+ setUrl,
112
+ isEditing,
113
+ onRemove,
114
+ onClose
115
+ }), [
116
+ label,
117
+ url,
118
+ isEditing,
119
+ onRemove,
120
+ onClose
121
+ ]),
122
+ children: /* @__PURE__ */ jsx(Form, {
123
+ onSubmit: handleSubmit,
124
+ className: "lp-popover-form",
125
+ children
126
+ })
127
+ });
128
+ }
129
+ function LinkPopoverRoot({ x, y, onClose, ...contentProps }) {
130
+ const virtualAnchor = useMemo(() => ({ getBoundingClientRect: () => ({
131
+ x,
132
+ y,
133
+ width: 0,
134
+ height: 0,
135
+ top: y,
136
+ left: x,
137
+ right: x,
138
+ bottom: y,
139
+ toJSON() {
140
+ return this;
141
+ }
142
+ }) }), [x, y]);
143
+ return /* @__PURE__ */ jsx(Popover.Root, {
144
+ open: true,
145
+ onOpenChange: (open) => {
146
+ if (!open) onClose();
147
+ },
148
+ children: /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
149
+ anchor: virtualAnchor,
150
+ positionMethod: "fixed",
151
+ side: "bottom",
152
+ align: "start",
153
+ sideOffset: 8,
154
+ className: "lp-popover-positioner",
155
+ children: /* @__PURE__ */ jsx(Popover.Popup, {
156
+ className: "lp-popover",
157
+ children: /* @__PURE__ */ jsx(LinkPopoverContent, {
158
+ onClose,
159
+ ...contentProps
160
+ })
161
+ })
162
+ }) })
163
+ });
164
+ }
165
+ function LinkPopoverConnected({ children }) {
166
+ const { view } = useEditorContext();
167
+ const popoverReq = useStateField(popoverField, null);
168
+ const linkReq = popoverReq?.type === "link" ? popoverReq : null;
169
+ const handleSubmit = useCallback((label, url) => {
170
+ if (!view || !linkReq) return;
171
+ const { from, to } = linkReq.data;
172
+ insertLink(view, from, to, label, url);
173
+ view.dispatch({ effects: closePopover.of() });
174
+ }, [view, linkReq]);
175
+ const handleRemove = useCallback(() => {
176
+ if (!view || !linkReq) return;
177
+ const { from, to, text } = linkReq.data;
178
+ view.dispatch({
179
+ changes: {
180
+ from,
181
+ to,
182
+ insert: text
183
+ },
184
+ selection: EditorSelection.cursor(from + text.length)
185
+ });
186
+ view.dispatch({ effects: closePopover.of() });
187
+ view.focus();
188
+ }, [view, linkReq]);
189
+ const handleClose = useCallback(() => {
190
+ if (!view) return;
191
+ view.dispatch({ effects: closePopover.of() });
192
+ view.focus();
193
+ }, [view]);
194
+ const virtualAnchor = useVirtualAnchor(linkReq?.data.from ?? null);
195
+ return /* @__PURE__ */ jsx(Popover.Root, {
196
+ open: !!linkReq,
197
+ onOpenChange: (open) => {
198
+ if (!open) handleClose();
199
+ },
200
+ children: /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
201
+ anchor: virtualAnchor,
202
+ positionMethod: "fixed",
203
+ side: "bottom",
204
+ align: "start",
205
+ sideOffset: 8,
206
+ className: "lp-popover-positioner",
207
+ children: /* @__PURE__ */ jsx(Popover.Popup, {
208
+ className: "lp-popover",
209
+ children: linkReq && /* @__PURE__ */ jsx(LinkPopoverContent, {
210
+ initialLabel: linkReq.data.text,
211
+ initialUrl: linkReq.data.url,
212
+ onSubmit: handleSubmit,
213
+ onRemove: handleRemove,
214
+ onClose: handleClose,
215
+ children
216
+ }, `${linkReq.data.from}-${linkReq.data.to}`)
217
+ })
218
+ }) })
219
+ });
220
+ }
221
+ //#endregion
222
+ export { Actions, LinkPopoverContent as Content, LabelInput, LinkPopoverConnected, LinkPopoverRoot as Root, UrlInput };
@@ -0,0 +1,13 @@
1
+ import { JSX } from "react";
2
+
3
+ //#region src/react/preview.d.ts
4
+ interface PreviewProps {
5
+ value: string;
6
+ className?: string;
7
+ }
8
+ declare function Preview({
9
+ value,
10
+ className
11
+ }: PreviewProps): JSX.Element;
12
+ //#endregion
13
+ export { Preview, PreviewProps };
@@ -0,0 +1,56 @@
1
+ import { highlightCode } from "../core/highlight.js";
2
+ import { useEffect, useState } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ import { Marked } from "marked";
5
+ import { markedHighlight } from "marked-highlight";
6
+ //#region src/react/preview.tsx
7
+ function parseFenceInfo(info) {
8
+ const parts = info.trim().split(/\s+/);
9
+ let lang = "";
10
+ let filename = "";
11
+ for (const part of parts) if (part.startsWith("filename=")) filename = part.slice(9);
12
+ else if (!lang) lang = part;
13
+ return {
14
+ lang,
15
+ filename
16
+ };
17
+ }
18
+ const marked = new Marked({ gfm: true }, markedHighlight({
19
+ async: true,
20
+ langPrefix: "language-",
21
+ async highlight(code, lang) {
22
+ const actualLang = lang.split(/\s+/)[0];
23
+ if (!actualLang) return code;
24
+ return highlightCode(code, actualLang);
25
+ }
26
+ }), { renderer: { code({ text, lang: rawLang }) {
27
+ const { lang, filename } = parseFenceInfo(rawLang || "");
28
+ let header = "";
29
+ if (filename || lang) header = `<div class="lp-code-header"><span>${filename || lang}</span>${filename && lang ? `<span class="lp-code-header-lang">${lang}</span>` : ""}</div>`;
30
+ const lines = text.split("\n");
31
+ if (lines.at(-1) === "") lines.pop();
32
+ const rows = lines.map((line, i) => `<span class="lp-code-row"><span class="lp-code-line-number">${i + 1}</span><span class="lp-code-line">${line || " "}</span></span>`).join("");
33
+ return `<div class="lp-code-block">${header}<pre><code class="language-${lang}">${rows}</code></pre></div>`;
34
+ } } });
35
+ function Preview({ value, className }) {
36
+ const [html, setHtml] = useState("");
37
+ useEffect(() => {
38
+ if (!value) {
39
+ setHtml("");
40
+ return;
41
+ }
42
+ let cancelled = false;
43
+ marked.parse(value).then((result) => {
44
+ if (!cancelled) setHtml(result);
45
+ });
46
+ return () => {
47
+ cancelled = true;
48
+ };
49
+ }, [value]);
50
+ return /* @__PURE__ */ jsx("div", {
51
+ className: `lp-preview ${className || ""}`,
52
+ dangerouslySetInnerHTML: { __html: html }
53
+ });
54
+ }
55
+ //#endregion
56
+ export { Preview };
@@ -0,0 +1,51 @@
1
+ import { ComponentProps, JSX, ReactNode } from "react";
2
+ import { Toolbar } from "@base-ui/react/toolbar";
3
+
4
+ //#region src/react/toolbar.d.ts
5
+ type InlineAction = "bold" | "italic" | "strikethrough" | "code" | "link";
6
+ type BlockAction = "heading1" | "heading2" | "heading3" | "orderedList" | "unorderedList" | "taskList" | "blockquote" | "codeblock";
7
+ interface ToolbarInlineButtonProps extends Omit<ComponentProps<typeof Toolbar.Button>, "children"> {
8
+ action: InlineAction;
9
+ children?: ReactNode;
10
+ }
11
+ declare function ToolbarInlineButton({
12
+ action,
13
+ children,
14
+ className,
15
+ ...props
16
+ }: ToolbarInlineButtonProps): JSX.Element;
17
+ interface ToolbarBlockButtonProps extends Omit<ComponentProps<typeof Toolbar.Button>, "children"> {
18
+ action: BlockAction;
19
+ children?: ReactNode;
20
+ }
21
+ declare function ToolbarBlockButton({
22
+ action,
23
+ children,
24
+ className,
25
+ ...props
26
+ }: ToolbarBlockButtonProps): JSX.Element;
27
+ type ToolbarButtonProps = ComponentProps<typeof Toolbar.Button>;
28
+ declare function ToolbarButton({
29
+ className,
30
+ ...props
31
+ }: ToolbarButtonProps): JSX.Element;
32
+ type ToolbarSeparatorProps = ComponentProps<typeof Toolbar.Separator>;
33
+ declare function ToolbarSeparator({
34
+ className,
35
+ ...props
36
+ }: ToolbarSeparatorProps): JSX.Element;
37
+ type ToolbarGroupProps = ComponentProps<typeof Toolbar.Group>;
38
+ declare function ToolbarGroup({
39
+ className,
40
+ ...props
41
+ }: ToolbarGroupProps): JSX.Element;
42
+ interface ToolbarProps extends Omit<ComponentProps<typeof Toolbar.Root>, "children"> {
43
+ children?: ReactNode;
44
+ }
45
+ declare function ToolbarRoot({
46
+ children,
47
+ className,
48
+ ...props
49
+ }: ToolbarProps): JSX.Element;
50
+ //#endregion
51
+ export { BlockAction, ToolbarBlockButton as BlockButton, ToolbarButton as Button, ToolbarGroup as Group, InlineAction, ToolbarInlineButton as InlineButton, ToolbarRoot as Root, ToolbarSeparator as Separator, ToolbarBlockButtonProps, ToolbarInlineButtonProps, ToolbarProps };
@@ -0,0 +1,161 @@
1
+ import { findCodeFenceAtCursor, findLinkAtCursor, toggleBlockquote, toggleHeading, toggleListKind, toggleMarker } from "../core/commands.js";
2
+ import { openPopover } from "../core/popover.js";
3
+ import { useActiveMarks, useEditorContext } from "./context.js";
4
+ import { useCallback } from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
+ import { Toolbar } from "@base-ui/react/toolbar";
7
+ //#region src/react/toolbar.tsx
8
+ const inlineCommands = {
9
+ bold: toggleMarker("**"),
10
+ italic: toggleMarker("_"),
11
+ strikethrough: toggleMarker("~~"),
12
+ code: toggleMarker("`")
13
+ };
14
+ const blockCommands = {
15
+ heading1: toggleHeading(1),
16
+ heading2: toggleHeading(2),
17
+ heading3: toggleHeading(3),
18
+ orderedList: toggleListKind("ul"),
19
+ unorderedList: toggleListKind("ol"),
20
+ taskList: toggleListKind("task"),
21
+ blockquote: toggleBlockquote
22
+ };
23
+ function preventFocusLoss(e) {
24
+ e.preventDefault();
25
+ }
26
+ function ToolbarInlineButton({ action, children, className, ...props }) {
27
+ const { view } = useEditorContext();
28
+ const marks = useActiveMarks();
29
+ const active = marks[action] ?? false;
30
+ const disabled = marks.codeblock ?? false;
31
+ const handleClick = useCallback(() => {
32
+ if (!view || disabled) return;
33
+ if (action === "link") {
34
+ const { state } = view;
35
+ const range = state.selection.main;
36
+ const existing = findLinkAtCursor(state, range.from);
37
+ const coords = view.coordsAtPos(range.from);
38
+ if (!coords) return;
39
+ const req = existing ? {
40
+ type: "link",
41
+ x: coords.left,
42
+ y: coords.bottom,
43
+ data: {
44
+ text: existing.label,
45
+ url: existing.url,
46
+ from: existing.from,
47
+ to: existing.to
48
+ }
49
+ } : {
50
+ type: "link",
51
+ x: coords.left,
52
+ y: coords.bottom,
53
+ data: {
54
+ text: state.sliceDoc(range.from, range.to),
55
+ url: "",
56
+ from: range.from,
57
+ to: range.to
58
+ }
59
+ };
60
+ view.dispatch({ effects: openPopover.of(req) });
61
+ } else {
62
+ inlineCommands[action](view);
63
+ view.focus();
64
+ }
65
+ }, [
66
+ view,
67
+ disabled,
68
+ action
69
+ ]);
70
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
71
+ disabled,
72
+ className: `lp-toolbar-btn${active ? " lp-toolbar-btn--active" : ""} ${className || ""}`,
73
+ onMouseDown: preventFocusLoss,
74
+ onClick: handleClick,
75
+ ...props,
76
+ children
77
+ });
78
+ }
79
+ function ToolbarBlockButton({ action, children, className, ...props }) {
80
+ const { view } = useEditorContext();
81
+ const marks = useActiveMarks();
82
+ const active = marks[action] ?? false;
83
+ const disabled = (marks.codeblock ?? false) && action !== "codeblock";
84
+ const handleClick = useCallback(() => {
85
+ if (!view || disabled) return;
86
+ if (action === "codeblock") {
87
+ const { state } = view;
88
+ const range = state.selection.main;
89
+ const coords = view.coordsAtPos(range.from);
90
+ if (!coords) return;
91
+ const existing = findCodeFenceAtCursor(view);
92
+ const req = existing ? {
93
+ type: "codeblock",
94
+ x: coords.left,
95
+ y: coords.bottom,
96
+ data: {
97
+ lang: existing.lang,
98
+ filename: existing.filename,
99
+ fenceFrom: existing.fenceLine.from,
100
+ fenceTo: existing.fenceLine.to,
101
+ isNew: false
102
+ }
103
+ } : {
104
+ type: "codeblock",
105
+ x: coords.left,
106
+ y: coords.bottom,
107
+ data: {
108
+ lang: "",
109
+ filename: "",
110
+ fenceFrom: 0,
111
+ fenceTo: 0,
112
+ isNew: true,
113
+ insertPos: range.from
114
+ }
115
+ };
116
+ view.dispatch({ effects: openPopover.of(req) });
117
+ return;
118
+ }
119
+ blockCommands[action](view);
120
+ view.focus();
121
+ }, [
122
+ view,
123
+ disabled,
124
+ action
125
+ ]);
126
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
127
+ disabled,
128
+ className: `lp-toolbar-btn${active ? " lp-toolbar-btn--active" : ""} ${className || ""}`,
129
+ onMouseDown: preventFocusLoss,
130
+ onClick: handleClick,
131
+ ...props,
132
+ children
133
+ });
134
+ }
135
+ function ToolbarButton({ className, ...props }) {
136
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
137
+ className: `lp-toolbar-btn ${className || ""}`,
138
+ ...props
139
+ });
140
+ }
141
+ function ToolbarSeparator({ className, ...props }) {
142
+ return /* @__PURE__ */ jsx(Toolbar.Separator, {
143
+ className: `lp-toolbar-separator ${className || ""}`,
144
+ ...props
145
+ });
146
+ }
147
+ function ToolbarGroup({ className, ...props }) {
148
+ return /* @__PURE__ */ jsx(Toolbar.Group, {
149
+ className: `lp-toolbar-group ${className || ""}`,
150
+ ...props
151
+ });
152
+ }
153
+ function ToolbarRoot({ children, className, ...props }) {
154
+ return /* @__PURE__ */ jsx(Toolbar.Root, {
155
+ className: `lp-toolbar ${className || ""}`,
156
+ ...props,
157
+ children
158
+ });
159
+ }
160
+ //#endregion
161
+ export { ToolbarBlockButton as BlockButton, ToolbarButton as Button, ToolbarGroup as Group, ToolbarInlineButton as InlineButton, ToolbarRoot as Root, ToolbarSeparator as Separator };
package/package.json CHANGED
@@ -1 +1,90 @@
1
- {"name":"leepi","version":"0.0.0","description":"Lipi - A composable React markdown editor","license":"MIT"}
1
+ {
2
+ "name": "leepi",
3
+ "version": "0.0.3",
4
+ "description": "A composable React markdown editor with inline styling",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/brijbyte/leepi.git",
8
+ "directory": "packages/leepi"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "src",
13
+ "!src/**/*.test.ts",
14
+ "!src/**/test-helpers.ts"
15
+ ],
16
+ "type": "module",
17
+ "exports": {
18
+ "./core/active-marks": "./dist/core/active-marks.js",
19
+ "./core/commands": "./dist/core/commands.js",
20
+ "./core/editor": "./dist/core/editor.js",
21
+ "./core/field-notifier": "./dist/core/field-notifier.js",
22
+ "./core/highlight": "./dist/core/highlight.js",
23
+ "./core/plugins": "./dist/core/plugins.js",
24
+ "./core/popover": "./dist/core/popover.js",
25
+ "./core/registry": "./dist/core/registry.js",
26
+ "./core/types": "./dist/core/types.js",
27
+ "./core/utils": "./dist/core/utils.js",
28
+ "./react/code-block-popover": "./dist/react/code-block-popover.js",
29
+ "./react/context": "./dist/react/context.js",
30
+ "./react/editor": "./dist/react/editor.js",
31
+ "./react/floating-toolbar": "./dist/react/floating-toolbar.js",
32
+ "./react/link-popover": "./dist/react/link-popover.js",
33
+ "./react/preview": "./dist/react/preview.js",
34
+ "./react/toolbar": "./dist/react/toolbar.js",
35
+ "./package.json": "./package.json",
36
+ "./styles.css": "./dist/leepi.css",
37
+ "./styles/*.css": "./src/styles/*.css"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "provenance": true
42
+ },
43
+ "dependencies": {
44
+ "@codemirror/commands": "^6.0.0",
45
+ "@codemirror/lang-markdown": "^6.0.0",
46
+ "@codemirror/language": "^6.0.0",
47
+ "@codemirror/language-data": "^6.0.0",
48
+ "@codemirror/state": "^6.0.0",
49
+ "@codemirror/view": "^6.0.0",
50
+ "@lezer/highlight": "^1.0.0",
51
+ "marked": ">=15.0.0",
52
+ "marked-highlight": "^2.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@base-ui/react": "^1.3.0",
56
+ "@codemirror/commands": "^6.10.3",
57
+ "@codemirror/lang-markdown": "^6.5.0",
58
+ "@codemirror/language": "^6.12.3",
59
+ "@codemirror/language-data": "^6.5.2",
60
+ "@codemirror/state": "^6.6.0",
61
+ "@codemirror/view": "^6.41.0",
62
+ "@lezer/highlight": "^1.2.3",
63
+ "@types/react": "^19.2.14",
64
+ "@types/react-dom": "^19.2.3",
65
+ "marked": ">=18.0.0",
66
+ "marked-highlight": "^2.2.3",
67
+ "react": "^19.2.4",
68
+ "react-dom": "^19.2.4"
69
+ },
70
+ "peerDependencies": {
71
+ "@base-ui/react": "^1.0.0",
72
+ "react": "^19.0.0",
73
+ "react-dom": "^19.0.0"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "@base-ui/react": {
77
+ "optional": true
78
+ },
79
+ "react": {
80
+ "optional": true
81
+ },
82
+ "react-dom": {
83
+ "optional": true
84
+ }
85
+ },
86
+ "scripts": {
87
+ "build": "tsdown",
88
+ "typecheck": "tsc --noEmit"
89
+ }
90
+ }
@@ -0,0 +1,89 @@
1
+ import { type Extension } from "@codemirror/state";
2
+ import { type EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
3
+ import { syntaxTree } from "@codemirror/language";
4
+ import { markRegistry } from "./registry";
5
+
6
+ /**
7
+ * Active marks as a dynamic record. Keys are mark names registered via `markRegistry`.
8
+ */
9
+ export type ActiveMarks = Record<string, boolean>;
10
+
11
+ export const emptyMarks: ActiveMarks = {};
12
+
13
+ export function detectMarks(view: EditorView): ActiveMarks {
14
+ const { state } = view;
15
+ const pos = state.selection.main.from;
16
+ const tree = syntaxTree(state);
17
+ const detectors = state.facet(markRegistry);
18
+
19
+ const marks: ActiveMarks = {};
20
+
21
+ // Tree walk: call all detectors with each node name
22
+ let node = tree.resolveInner(pos, -1);
23
+ while (node) {
24
+ for (const det of detectors) {
25
+ if (!marks[det.mark] && det.detect(node.name, state, pos)) {
26
+ marks[det.mark] = true;
27
+ }
28
+ }
29
+ if (!node.parent) break;
30
+ node = node.parent;
31
+ }
32
+
33
+ // Line pass: call detectors with "__line__" sentinel for line-level checks
34
+ for (const det of detectors) {
35
+ if (!marks[det.mark] && det.detect("__line__", state, pos)) {
36
+ marks[det.mark] = true;
37
+ }
38
+ }
39
+
40
+ return marks;
41
+ }
42
+
43
+ // Notification system: views notify React subscribers when marks change
44
+ const viewListeners = new WeakMap<EditorView, Set<() => void>>();
45
+ const viewSnapshots = new WeakMap<EditorView, ActiveMarks>();
46
+
47
+ export function subscribeToMarks(view: EditorView, callback: () => void): () => void {
48
+ let listeners = viewListeners.get(view);
49
+ if (!listeners) {
50
+ listeners = new Set();
51
+ viewListeners.set(view, listeners);
52
+ }
53
+ listeners.add(callback);
54
+ return () => {
55
+ listeners!.delete(callback);
56
+ };
57
+ }
58
+
59
+ export function getMarksSnapshot(view: EditorView): ActiveMarks {
60
+ return viewSnapshots.get(view) ?? emptyMarks;
61
+ }
62
+
63
+ function marksEqual(a: ActiveMarks, b: ActiveMarks): boolean {
64
+ const aKeys = Object.keys(a);
65
+ const bKeys = Object.keys(b);
66
+ if (aKeys.length !== bKeys.length) return false;
67
+ for (const key of aKeys) {
68
+ if (a[key] !== b[key]) return false;
69
+ }
70
+ return true;
71
+ }
72
+
73
+ export const activeMarksPlugin: Extension = ViewPlugin.fromClass(
74
+ class {
75
+ update(update: ViewUpdate): void {
76
+ if (update.selectionSet || update.docChanged) {
77
+ const marks = detectMarks(update.view);
78
+ const prev = viewSnapshots.get(update.view) ?? emptyMarks;
79
+ if (!marksEqual(prev, marks)) {
80
+ viewSnapshots.set(update.view, marks);
81
+ const listeners = viewListeners.get(update.view);
82
+ if (listeners) {
83
+ for (const cb of listeners) cb();
84
+ }
85
+ }
86
+ }
87
+ }
88
+ },
89
+ );