@junco/editor 0.0.0-stage → 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 +15 -2
- package/dist/index.d.ts +78 -0
- package/dist/index.js +387 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oluwatobi Olajide
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @junco/editor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A rich text editor for [Junco](https://junco.madebytobi.com) on [TipTap](https://tiptap.dev): a formatting toolbar, a `/` command menu and markdown shortcuts, styled by Junco's tokens in light and dark. Give it a Y.Doc and it's collaborative, with everyone's carets.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Editor } from "@junco/editor";
|
|
7
|
+
|
|
8
|
+
<Editor content="<p>Hello</p>" onUpdate={(editor) => save(editor.getJSON())} />;
|
|
9
|
+
|
|
10
|
+
// Together, on Towhee (or any Yjs provider):
|
|
11
|
+
<Editor doc={room.doc} provider={room} />;
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Also: `Toolbar`, `SlashMenu`, `defaultSlashCommands` and `editorText(doc)` (a document as plain text, for version compare).
|
|
15
|
+
|
|
16
|
+
Docs: [junco.madebytobi.com/docs/editor](https://junco.madebytobi.com/docs/editor/) · Live: [junco.madebytobi.com/editor](https://junco.madebytobi.com/editor/)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Editor as Editor$1, Extensions } from '@tiptap/react';
|
|
2
|
+
export { Editor as TiptapEditor } from '@tiptap/react';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as Y from 'yjs';
|
|
5
|
+
|
|
6
|
+
interface SlashCommand {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
/** Extra words to match while typing. */
|
|
10
|
+
keywords?: string;
|
|
11
|
+
icon?: React.ReactNode;
|
|
12
|
+
/** Runs after the "/query" text is removed. */
|
|
13
|
+
run: (editor: Editor$1) => void;
|
|
14
|
+
}
|
|
15
|
+
declare const defaultSlashCommands: SlashCommand[];
|
|
16
|
+
/** The "/" command menu. Arrow keys move, Enter runs, Escape closes. Shown by `Editor`. */
|
|
17
|
+
declare function SlashMenu({ editor, commands }: {
|
|
18
|
+
editor: Editor$1;
|
|
19
|
+
commands?: SlashCommand[];
|
|
20
|
+
}): React.ReactPortal | null;
|
|
21
|
+
|
|
22
|
+
/** Anything with a y-protocols Awareness: a Towhee room, a y-websocket or Hocuspocus provider. */
|
|
23
|
+
interface AwarenessProvider {
|
|
24
|
+
awareness: any;
|
|
25
|
+
}
|
|
26
|
+
interface EditorProps {
|
|
27
|
+
/** Starting content (HTML or TipTap JSON) when not collaborating. Ignored with `doc`: the document is the content. */
|
|
28
|
+
content?: string | Record<string, unknown>;
|
|
29
|
+
/** A shared Y.Doc to edit together. */
|
|
30
|
+
doc?: Y.Doc;
|
|
31
|
+
/** The fragment in `doc` this editor owns. Default `"default"`. */
|
|
32
|
+
field?: string;
|
|
33
|
+
/** Shows everyone's carets and selections. Pass the room or provider that holds the Awareness. */
|
|
34
|
+
provider?: AwarenessProvider;
|
|
35
|
+
/** You, as others see your caret. Towhee fills this in from the token, so it's optional there. */
|
|
36
|
+
user?: {
|
|
37
|
+
name: string;
|
|
38
|
+
color?: string;
|
|
39
|
+
};
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
editable?: boolean;
|
|
42
|
+
/** Default true. */
|
|
43
|
+
toolbar?: boolean;
|
|
44
|
+
/** Commands for the "/" menu. Default: text, headings, lists, quote, code, divider. Pass [] to turn it off. */
|
|
45
|
+
slashCommands?: SlashCommand[];
|
|
46
|
+
/** Extra TipTap extensions. */
|
|
47
|
+
extensions?: Extensions;
|
|
48
|
+
onReady?: (editor: Editor$1) => void;
|
|
49
|
+
onUpdate?: (editor: Editor$1) => void;
|
|
50
|
+
/** Accessible name for the editing area. Default "Document". */
|
|
51
|
+
label?: string;
|
|
52
|
+
className?: string;
|
|
53
|
+
classNames?: {
|
|
54
|
+
root?: string;
|
|
55
|
+
toolbar?: string;
|
|
56
|
+
content?: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A rich text editor: bold, italic, headings, lists, quotes, code, links, markdown shortcuts and a
|
|
61
|
+
* "/" command menu. Give it a Y.Doc (and the room that holds its Awareness) and everyone edits
|
|
62
|
+
* the same document with their carets showing.
|
|
63
|
+
*/
|
|
64
|
+
declare function Editor({ content, doc, field, provider, user, placeholder, editable, toolbar, slashCommands, extensions, onReady, onUpdate, label, className, classNames, }: EditorProps): React.JSX.Element;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A collaborative editor's document as plain text, one block per line. Use it to compare versions:
|
|
68
|
+
* `towheeAdapter({ room, textOf: (doc) => editorText(doc) })`.
|
|
69
|
+
*/
|
|
70
|
+
declare function editorText(doc: Y.Doc, field?: string): string;
|
|
71
|
+
|
|
72
|
+
/** Formatting buttons that reflect the selection. Shown by `Editor`; use it on its own with any TipTap editor. */
|
|
73
|
+
declare function Toolbar({ editor, className }: {
|
|
74
|
+
editor: Editor$1;
|
|
75
|
+
className?: string;
|
|
76
|
+
}): React.JSX.Element;
|
|
77
|
+
|
|
78
|
+
export { type AwarenessProvider, Editor, type EditorProps, type SlashCommand, SlashMenu, Toolbar, defaultSlashCommands, editorText };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/editor.tsx
|
|
4
|
+
import { cn as cn3 } from "@junco/react";
|
|
5
|
+
import Collaboration from "@tiptap/extension-collaboration";
|
|
6
|
+
import CollaborationCaret from "@tiptap/extension-collaboration-caret";
|
|
7
|
+
import Placeholder from "@tiptap/extension-placeholder";
|
|
8
|
+
import { EditorContent, useEditor } from "@tiptap/react";
|
|
9
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
10
|
+
import * as React2 from "react";
|
|
11
|
+
|
|
12
|
+
// src/slash.tsx
|
|
13
|
+
import { cn } from "@junco/react";
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
import { createPortal } from "react-dom";
|
|
16
|
+
|
|
17
|
+
// src/icons.tsx
|
|
18
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
19
|
+
function make(paths) {
|
|
20
|
+
return (props) => /* @__PURE__ */ jsx(
|
|
21
|
+
"svg",
|
|
22
|
+
{
|
|
23
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
24
|
+
viewBox: "0 0 24 24",
|
|
25
|
+
fill: "none",
|
|
26
|
+
stroke: "currentColor",
|
|
27
|
+
strokeWidth: 2,
|
|
28
|
+
strokeLinecap: "round",
|
|
29
|
+
strokeLinejoin: "round",
|
|
30
|
+
"aria-hidden": true,
|
|
31
|
+
focusable: false,
|
|
32
|
+
...props,
|
|
33
|
+
children: paths
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
var BoldIcon = make(/* @__PURE__ */ jsx("path", { d: "M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" }));
|
|
38
|
+
var ItalicIcon = make(
|
|
39
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
40
|
+
/* @__PURE__ */ jsx("line", { x1: "19", x2: "10", y1: "4", y2: "4" }),
|
|
41
|
+
/* @__PURE__ */ jsx("line", { x1: "14", x2: "5", y1: "20", y2: "20" }),
|
|
42
|
+
/* @__PURE__ */ jsx("line", { x1: "15", x2: "9", y1: "4", y2: "20" })
|
|
43
|
+
] })
|
|
44
|
+
);
|
|
45
|
+
var StrikeIcon = make(
|
|
46
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
47
|
+
/* @__PURE__ */ jsx("path", { d: "M16 4H9a3 3 0 0 0-2.83 4" }),
|
|
48
|
+
/* @__PURE__ */ jsx("path", { d: "M14 12a4 4 0 0 1 0 8H6" }),
|
|
49
|
+
/* @__PURE__ */ jsx("line", { x1: "4", x2: "20", y1: "12", y2: "12" })
|
|
50
|
+
] })
|
|
51
|
+
);
|
|
52
|
+
var CodeIcon = make(
|
|
53
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
54
|
+
/* @__PURE__ */ jsx("path", { d: "m16 18 6-6-6-6" }),
|
|
55
|
+
/* @__PURE__ */ jsx("path", { d: "m8 6-6 6 6 6" })
|
|
56
|
+
] })
|
|
57
|
+
);
|
|
58
|
+
var ListIcon = make(/* @__PURE__ */ jsx("path", { d: "M3 12h.01M3 18h.01M3 6h.01M8 12h13M8 18h13M8 6h13" }));
|
|
59
|
+
var ListOrderedIcon = make(/* @__PURE__ */ jsx("path", { d: "M10 12h11M10 18h11M10 6h11M4 10h2M4 6h1v4M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" }));
|
|
60
|
+
var QuoteIcon = make(
|
|
61
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
62
|
+
/* @__PURE__ */ jsx("path", { d: "M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z" }),
|
|
63
|
+
/* @__PURE__ */ jsx("path", { d: "M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z" })
|
|
64
|
+
] })
|
|
65
|
+
);
|
|
66
|
+
var CodeBlockIcon = make(
|
|
67
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
68
|
+
/* @__PURE__ */ jsx("path", { d: "m10 9-3 3 3 3M14 15l3-3-3-3" }),
|
|
69
|
+
/* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" })
|
|
70
|
+
] })
|
|
71
|
+
);
|
|
72
|
+
var MinusIcon = make(/* @__PURE__ */ jsx("path", { d: "M5 12h14" }));
|
|
73
|
+
var TextIcon = make(/* @__PURE__ */ jsx("path", { d: "M17 6.1H3M21 12.1H3M15.1 18H3" }));
|
|
74
|
+
var UndoIcon = make(
|
|
75
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
76
|
+
/* @__PURE__ */ jsx("path", { d: "M9 14 4 9l5-5" }),
|
|
77
|
+
/* @__PURE__ */ jsx("path", { d: "M4 9h10.5a5.5 5.5 0 0 1 0 11H11" })
|
|
78
|
+
] })
|
|
79
|
+
);
|
|
80
|
+
var RedoIcon = make(
|
|
81
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
82
|
+
/* @__PURE__ */ jsx("path", { d: "m15 14 5-5-5-5" }),
|
|
83
|
+
/* @__PURE__ */ jsx("path", { d: "M20 9H9.5a5.5 5.5 0 0 0 0 11H13" })
|
|
84
|
+
] })
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// src/slash.tsx
|
|
88
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
89
|
+
var heading = (level) => ({
|
|
90
|
+
id: `h${level}`,
|
|
91
|
+
label: `Heading ${level}`,
|
|
92
|
+
keywords: `title h${level}`,
|
|
93
|
+
icon: /* @__PURE__ */ jsxs2("span", { className: "text-xs font-semibold", children: [
|
|
94
|
+
"H",
|
|
95
|
+
level
|
|
96
|
+
] }),
|
|
97
|
+
run: (e) => e.chain().focus().setHeading({ level }).run()
|
|
98
|
+
});
|
|
99
|
+
var defaultSlashCommands = [
|
|
100
|
+
{ id: "text", label: "Text", keywords: "paragraph plain", icon: /* @__PURE__ */ jsx2(TextIcon, { className: "size-4" }), run: (e) => e.chain().focus().setParagraph().run() },
|
|
101
|
+
heading(1),
|
|
102
|
+
heading(2),
|
|
103
|
+
heading(3),
|
|
104
|
+
{ id: "bullet", label: "Bulleted list", keywords: "ul unordered", icon: /* @__PURE__ */ jsx2(ListIcon, { className: "size-4" }), run: (e) => e.chain().focus().toggleBulletList().run() },
|
|
105
|
+
{ id: "ordered", label: "Numbered list", keywords: "ol ordered", icon: /* @__PURE__ */ jsx2(ListOrderedIcon, { className: "size-4" }), run: (e) => e.chain().focus().toggleOrderedList().run() },
|
|
106
|
+
{ id: "quote", label: "Quote", keywords: "blockquote", icon: /* @__PURE__ */ jsx2(QuoteIcon, { className: "size-4" }), run: (e) => e.chain().focus().setBlockquote().run() },
|
|
107
|
+
{ id: "code", label: "Code block", keywords: "pre snippet", icon: /* @__PURE__ */ jsx2(CodeBlockIcon, { className: "size-4" }), run: (e) => e.chain().focus().setCodeBlock().run() },
|
|
108
|
+
{ id: "divider", label: "Divider", keywords: "hr rule line", icon: /* @__PURE__ */ jsx2(MinusIcon, { className: "size-4" }), run: (e) => e.chain().focus().setHorizontalRule().run() }
|
|
109
|
+
];
|
|
110
|
+
function findSlash(editor) {
|
|
111
|
+
const { state } = editor;
|
|
112
|
+
const { $from, empty } = state.selection;
|
|
113
|
+
if (!empty || $from.parent.type.spec.code) return null;
|
|
114
|
+
const before = $from.parent.textBetween(0, $from.parentOffset, void 0, "\uFFFC");
|
|
115
|
+
const match = /(?:^|\s)\/([\w-]{0,24})$/.exec(before);
|
|
116
|
+
if (!match) return null;
|
|
117
|
+
return { from: $from.pos - match[1].length - 1, to: $from.pos, query: match[1].toLowerCase() };
|
|
118
|
+
}
|
|
119
|
+
function SlashMenu({ editor, commands = defaultSlashCommands }) {
|
|
120
|
+
const [open, setOpen] = React.useState(null);
|
|
121
|
+
const [index, setIndex] = React.useState(0);
|
|
122
|
+
const [dismissedAt, setDismissedAt] = React.useState(null);
|
|
123
|
+
const listId = React.useId();
|
|
124
|
+
const matches = React.useMemo(
|
|
125
|
+
() => open ? commands.filter((c) => `${c.label} ${c.keywords ?? ""}`.toLowerCase().includes(open.query)) : [],
|
|
126
|
+
[commands, open]
|
|
127
|
+
);
|
|
128
|
+
React.useEffect(() => {
|
|
129
|
+
if (commands.length === 0) return;
|
|
130
|
+
const update = () => {
|
|
131
|
+
const found = findSlash(editor);
|
|
132
|
+
if (!found || found.from === dismissedAt) {
|
|
133
|
+
setOpen(null);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const at = editor.view.coordsAtPos(found.from);
|
|
137
|
+
setOpen((prev) => {
|
|
138
|
+
if (!prev || prev.from !== found.from || prev.query !== found.query) setIndex(0);
|
|
139
|
+
return { ...found, left: at.left, top: at.bottom + 6 };
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
const close = () => setOpen(null);
|
|
143
|
+
editor.on("transaction", update);
|
|
144
|
+
editor.on("blur", close);
|
|
145
|
+
return () => {
|
|
146
|
+
editor.off("transaction", update);
|
|
147
|
+
editor.off("blur", close);
|
|
148
|
+
};
|
|
149
|
+
}, [editor, commands.length, dismissedAt]);
|
|
150
|
+
const choose = React.useCallback(
|
|
151
|
+
(command) => {
|
|
152
|
+
if (!command || !open) return;
|
|
153
|
+
editor.chain().focus().deleteRange({ from: open.from, to: open.to }).run();
|
|
154
|
+
command.run(editor);
|
|
155
|
+
setOpen(null);
|
|
156
|
+
},
|
|
157
|
+
[editor, open]
|
|
158
|
+
);
|
|
159
|
+
React.useEffect(() => {
|
|
160
|
+
if (!open || matches.length === 0) return;
|
|
161
|
+
const onKey = (e) => {
|
|
162
|
+
const handled = ["ArrowDown", "ArrowUp", "Enter", "Tab", "Escape"].includes(e.key);
|
|
163
|
+
if (handled) {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
e.stopImmediatePropagation();
|
|
166
|
+
}
|
|
167
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
168
|
+
const step = e.key === "ArrowDown" ? 1 : -1;
|
|
169
|
+
setIndex((i) => (i + step + matches.length) % matches.length);
|
|
170
|
+
} else if (e.key === "Enter" || e.key === "Tab") {
|
|
171
|
+
choose(matches[index]);
|
|
172
|
+
} else if (e.key === "Escape") {
|
|
173
|
+
setDismissedAt(open.from);
|
|
174
|
+
setOpen(null);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const dom = editor.view.dom;
|
|
178
|
+
dom.addEventListener("keydown", onKey, true);
|
|
179
|
+
return () => dom.removeEventListener("keydown", onKey, true);
|
|
180
|
+
}, [editor, open, matches, index, choose]);
|
|
181
|
+
React.useEffect(() => {
|
|
182
|
+
if (dismissedAt !== null && findSlash(editor)?.from !== dismissedAt) setDismissedAt(null);
|
|
183
|
+
});
|
|
184
|
+
React.useEffect(() => {
|
|
185
|
+
editor.view.dom.setAttribute("aria-expanded", String(Boolean(open && matches.length)));
|
|
186
|
+
if (open && matches.length) {
|
|
187
|
+
editor.view.dom.setAttribute("aria-controls", listId);
|
|
188
|
+
editor.view.dom.setAttribute("aria-activedescendant", `${listId}-${matches[index]?.id}`);
|
|
189
|
+
} else {
|
|
190
|
+
editor.view.dom.removeAttribute("aria-activedescendant");
|
|
191
|
+
}
|
|
192
|
+
}, [editor, open, matches, index, listId]);
|
|
193
|
+
if (!open || matches.length === 0 || typeof document === "undefined") return null;
|
|
194
|
+
return createPortal(
|
|
195
|
+
/* @__PURE__ */ jsx2(
|
|
196
|
+
"div",
|
|
197
|
+
{
|
|
198
|
+
id: listId,
|
|
199
|
+
role: "listbox",
|
|
200
|
+
"aria-label": "Commands",
|
|
201
|
+
className: "fixed z-popover w-60 animate-pop-in overflow-hidden rounded-card border border-border bg-popover p-1 text-popover-foreground shadow-float",
|
|
202
|
+
style: { left: open.left, top: open.top },
|
|
203
|
+
children: matches.map((c, i) => /* @__PURE__ */ jsxs2(
|
|
204
|
+
"div",
|
|
205
|
+
{
|
|
206
|
+
id: `${listId}-${c.id}`,
|
|
207
|
+
role: "option",
|
|
208
|
+
tabIndex: -1,
|
|
209
|
+
"aria-selected": i === index,
|
|
210
|
+
onMouseDown: (e) => {
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
choose(c);
|
|
213
|
+
},
|
|
214
|
+
onMouseEnter: () => setIndex(i),
|
|
215
|
+
className: cn("flex h-9 cursor-default items-center gap-2.5 rounded-control px-2 text-sm", i === index && "bg-accent text-accent-foreground"),
|
|
216
|
+
children: [
|
|
217
|
+
/* @__PURE__ */ jsx2("span", { className: "grid size-6 place-items-center rounded-xs border border-border bg-surface text-muted-foreground", children: c.icon }),
|
|
218
|
+
c.label
|
|
219
|
+
]
|
|
220
|
+
},
|
|
221
|
+
c.id
|
|
222
|
+
))
|
|
223
|
+
}
|
|
224
|
+
),
|
|
225
|
+
document.body
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/toolbar.tsx
|
|
230
|
+
import { cn as cn2, Tooltip } from "@junco/react";
|
|
231
|
+
import { useEditorState } from "@tiptap/react";
|
|
232
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
233
|
+
var mod = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "\u2318" : "Ctrl+";
|
|
234
|
+
function Tool({ label, shortcut, active, disabled, onClick, children }) {
|
|
235
|
+
return /* @__PURE__ */ jsx3(Tooltip, { content: shortcut ? `${label} (${shortcut})` : label, children: /* @__PURE__ */ jsx3(
|
|
236
|
+
"button",
|
|
237
|
+
{
|
|
238
|
+
type: "button",
|
|
239
|
+
"aria-label": label,
|
|
240
|
+
"aria-pressed": active === void 0 ? void 0 : active,
|
|
241
|
+
disabled,
|
|
242
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
243
|
+
onClick,
|
|
244
|
+
className: cn2(
|
|
245
|
+
"inline-grid size-8 place-items-center rounded-control text-muted-foreground transition-colors duration-fast ease-standard",
|
|
246
|
+
"hover:bg-accent hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-40",
|
|
247
|
+
active && "bg-accent text-foreground"
|
|
248
|
+
),
|
|
249
|
+
children
|
|
250
|
+
}
|
|
251
|
+
) });
|
|
252
|
+
}
|
|
253
|
+
var Divider = () => /* @__PURE__ */ jsx3("span", { "aria-hidden": true, className: "mx-1 h-5 w-px bg-border" });
|
|
254
|
+
function Toolbar({ editor, className }) {
|
|
255
|
+
const s = useEditorState({
|
|
256
|
+
editor,
|
|
257
|
+
selector: ({ editor: e }) => ({
|
|
258
|
+
bold: e.isActive("bold"),
|
|
259
|
+
italic: e.isActive("italic"),
|
|
260
|
+
strike: e.isActive("strike"),
|
|
261
|
+
code: e.isActive("code"),
|
|
262
|
+
h1: e.isActive("heading", { level: 1 }),
|
|
263
|
+
h2: e.isActive("heading", { level: 2 }),
|
|
264
|
+
h3: e.isActive("heading", { level: 3 }),
|
|
265
|
+
bullet: e.isActive("bulletList"),
|
|
266
|
+
ordered: e.isActive("orderedList"),
|
|
267
|
+
quote: e.isActive("blockquote"),
|
|
268
|
+
codeBlock: e.isActive("codeBlock"),
|
|
269
|
+
canUndo: e.can().undo(),
|
|
270
|
+
canRedo: e.can().redo()
|
|
271
|
+
})
|
|
272
|
+
});
|
|
273
|
+
const run = (fn) => fn(editor.chain().focus()).run();
|
|
274
|
+
return /* @__PURE__ */ jsxs3("div", { role: "toolbar", "aria-label": "Formatting", className: cn2("flex flex-wrap items-center gap-0.5 border-b border-border bg-surface px-2 py-1.5", className), children: [
|
|
275
|
+
/* @__PURE__ */ jsx3(Tool, { label: "Bold", shortcut: `${mod}B`, active: s.bold, onClick: () => run((c) => c.toggleBold()), children: /* @__PURE__ */ jsx3(BoldIcon, { className: "size-4" }) }),
|
|
276
|
+
/* @__PURE__ */ jsx3(Tool, { label: "Italic", shortcut: `${mod}I`, active: s.italic, onClick: () => run((c) => c.toggleItalic()), children: /* @__PURE__ */ jsx3(ItalicIcon, { className: "size-4" }) }),
|
|
277
|
+
/* @__PURE__ */ jsx3(Tool, { label: "Strikethrough", active: s.strike, onClick: () => run((c) => c.toggleStrike()), children: /* @__PURE__ */ jsx3(StrikeIcon, { className: "size-4" }) }),
|
|
278
|
+
/* @__PURE__ */ jsx3(Tool, { label: "Inline code", active: s.code, onClick: () => run((c) => c.toggleCode()), children: /* @__PURE__ */ jsx3(CodeIcon, { className: "size-4" }) }),
|
|
279
|
+
/* @__PURE__ */ jsx3(Divider, {}),
|
|
280
|
+
[1, 2, 3].map((level) => /* @__PURE__ */ jsx3(Tool, { label: `Heading ${level}`, active: s[`h${level}`], onClick: () => run((c) => c.toggleHeading({ level })), children: /* @__PURE__ */ jsxs3("span", { className: "text-xs font-semibold", children: [
|
|
281
|
+
"H",
|
|
282
|
+
level
|
|
283
|
+
] }) }, level)),
|
|
284
|
+
/* @__PURE__ */ jsx3(Divider, {}),
|
|
285
|
+
/* @__PURE__ */ jsx3(Tool, { label: "Bulleted list", active: s.bullet, onClick: () => run((c) => c.toggleBulletList()), children: /* @__PURE__ */ jsx3(ListIcon, { className: "size-4" }) }),
|
|
286
|
+
/* @__PURE__ */ jsx3(Tool, { label: "Numbered list", active: s.ordered, onClick: () => run((c) => c.toggleOrderedList()), children: /* @__PURE__ */ jsx3(ListOrderedIcon, { className: "size-4" }) }),
|
|
287
|
+
/* @__PURE__ */ jsx3(Tool, { label: "Quote", active: s.quote, onClick: () => run((c) => c.toggleBlockquote()), children: /* @__PURE__ */ jsx3(QuoteIcon, { className: "size-4" }) }),
|
|
288
|
+
/* @__PURE__ */ jsx3(Tool, { label: "Code block", active: s.codeBlock, onClick: () => run((c) => c.toggleCodeBlock()), children: /* @__PURE__ */ jsx3(CodeBlockIcon, { className: "size-4" }) }),
|
|
289
|
+
/* @__PURE__ */ jsx3(Divider, {}),
|
|
290
|
+
/* @__PURE__ */ jsx3(Tool, { label: "Undo", shortcut: `${mod}Z`, disabled: !s.canUndo, onClick: () => run((c) => c.undo()), children: /* @__PURE__ */ jsx3(UndoIcon, { className: "size-4" }) }),
|
|
291
|
+
/* @__PURE__ */ jsx3(Tool, { label: "Redo", shortcut: `${mod}Shift+Z`, disabled: !s.canRedo, onClick: () => run((c) => c.redo()), children: /* @__PURE__ */ jsx3(RedoIcon, { className: "size-4" }) })
|
|
292
|
+
] });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/editor.tsx
|
|
296
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
297
|
+
var proseClasses = cn3(
|
|
298
|
+
"min-h-48 px-4 py-3 text-[15px] leading-7 text-foreground outline-none",
|
|
299
|
+
"[&_.ProseMirror]:min-h-44 [&_.ProseMirror]:outline-none [&_.ProseMirror>*+*]:mt-3",
|
|
300
|
+
"[&_h1]:text-2xl [&_h1]:font-semibold [&_h1]:tracking-tight [&_h2]:text-xl [&_h2]:font-semibold [&_h3]:text-lg [&_h3]:font-semibold",
|
|
301
|
+
"[&_ul]:list-disc [&_ul]:pl-6 [&_ol]:list-decimal [&_ol]:pl-6 [&_li>p]:my-0",
|
|
302
|
+
"[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground",
|
|
303
|
+
"[&_code]:rounded-xs [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em]",
|
|
304
|
+
"[&_pre]:rounded-card [&_pre]:border [&_pre]:border-border [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:font-mono [&_pre]:text-sm [&_pre_code]:bg-transparent [&_pre_code]:p-0",
|
|
305
|
+
"[&_hr]:my-6 [&_hr]:border-border [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4",
|
|
306
|
+
"[&_p.is-editor-empty:first-child]:before:pointer-events-none [&_p.is-editor-empty:first-child]:before:float-left [&_p.is-editor-empty:first-child]:before:h-0 [&_p.is-editor-empty:first-child]:before:text-muted-foreground [&_p.is-editor-empty:first-child]:before:content-[attr(data-placeholder)]",
|
|
307
|
+
"[&_.collaboration-carets\\_\\_caret]:relative [&_.collaboration-carets\\_\\_caret]:-mx-px [&_.collaboration-carets\\_\\_caret]:border-x [&_.collaboration-carets\\_\\_caret]:border-solid",
|
|
308
|
+
"[&_.collaboration-carets\\_\\_label]:absolute [&_.collaboration-carets\\_\\_label]:-top-5 [&_.collaboration-carets\\_\\_label]:-left-px [&_.collaboration-carets\\_\\_label]:rounded-xs [&_.collaboration-carets\\_\\_label]:border-2 [&_.collaboration-carets\\_\\_label]:bg-popover [&_.collaboration-carets\\_\\_label]:px-1.5 [&_.collaboration-carets\\_\\_label]:text-[11px] [&_.collaboration-carets\\_\\_label]:leading-4 [&_.collaboration-carets\\_\\_label]:font-medium [&_.collaboration-carets\\_\\_label]:whitespace-nowrap [&_.collaboration-carets\\_\\_label]:text-popover-foreground [&_.collaboration-carets\\_\\_label]:select-none"
|
|
309
|
+
);
|
|
310
|
+
function caret(user) {
|
|
311
|
+
const color = typeof user.color === "string" ? user.color : "var(--primary)";
|
|
312
|
+
const cursor = document.createElement("span");
|
|
313
|
+
cursor.className = "collaboration-carets__caret";
|
|
314
|
+
cursor.style.borderColor = color;
|
|
315
|
+
const label = document.createElement("span");
|
|
316
|
+
label.className = "collaboration-carets__label";
|
|
317
|
+
label.style.borderColor = color;
|
|
318
|
+
label.textContent = typeof user.name === "string" ? user.name : "Someone";
|
|
319
|
+
cursor.append(label);
|
|
320
|
+
return cursor;
|
|
321
|
+
}
|
|
322
|
+
function Editor({
|
|
323
|
+
content,
|
|
324
|
+
doc,
|
|
325
|
+
field = "default",
|
|
326
|
+
provider,
|
|
327
|
+
user,
|
|
328
|
+
placeholder = "Write something, or press / for commands",
|
|
329
|
+
editable = true,
|
|
330
|
+
toolbar = true,
|
|
331
|
+
slashCommands,
|
|
332
|
+
extensions = [],
|
|
333
|
+
onReady,
|
|
334
|
+
onUpdate,
|
|
335
|
+
label = "Document",
|
|
336
|
+
className,
|
|
337
|
+
classNames
|
|
338
|
+
}) {
|
|
339
|
+
const callbacks = React2.useRef({ onReady, onUpdate });
|
|
340
|
+
callbacks.current = { onReady, onUpdate };
|
|
341
|
+
const editor = useEditor(
|
|
342
|
+
{
|
|
343
|
+
immediatelyRender: false,
|
|
344
|
+
editable,
|
|
345
|
+
content: doc ? void 0 : content,
|
|
346
|
+
extensions: [
|
|
347
|
+
// Yjs brings its own undo, which only undoes your changes; ProseMirror's would undo everyone's.
|
|
348
|
+
StarterKit.configure({ undoRedo: doc ? false : void 0, link: { openOnClick: false } }),
|
|
349
|
+
Placeholder.configure({ placeholder }),
|
|
350
|
+
...doc ? [Collaboration.configure({ document: doc, field })] : [],
|
|
351
|
+
...doc && provider ? [CollaborationCaret.configure({ provider, user: user ?? {}, render: caret })] : [],
|
|
352
|
+
...extensions
|
|
353
|
+
],
|
|
354
|
+
editorProps: { attributes: { "aria-label": label, "aria-multiline": "true", role: "textbox" } },
|
|
355
|
+
onCreate: ({ editor: e }) => callbacks.current.onReady?.(e),
|
|
356
|
+
onUpdate: ({ editor: e }) => callbacks.current.onUpdate?.(e)
|
|
357
|
+
},
|
|
358
|
+
[doc, field, provider]
|
|
359
|
+
);
|
|
360
|
+
React2.useEffect(() => {
|
|
361
|
+
editor?.setEditable(editable);
|
|
362
|
+
}, [editor, editable]);
|
|
363
|
+
return /* @__PURE__ */ jsxs4("div", { className: cn3("grid overflow-hidden rounded-card border border-border bg-surface", className, classNames?.root), children: [
|
|
364
|
+
toolbar && editor && editable && /* @__PURE__ */ jsx4(Toolbar, { editor, className: classNames?.toolbar }),
|
|
365
|
+
/* @__PURE__ */ jsx4(EditorContent, { editor, className: cn3(proseClasses, classNames?.content) }),
|
|
366
|
+
editor && editable && /* @__PURE__ */ jsx4(SlashMenu, { editor, commands: slashCommands })
|
|
367
|
+
] });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/text.ts
|
|
371
|
+
import * as Y from "yjs";
|
|
372
|
+
function elementText(el) {
|
|
373
|
+
const children = el.toArray();
|
|
374
|
+
const blocks = children.some((c) => c instanceof Y.XmlElement);
|
|
375
|
+
return children.map((c) => c instanceof Y.XmlText ? c.toDelta().map((d) => typeof d.insert === "string" ? d.insert : "").join("") : elementText(c)).join(blocks ? "\n" : "");
|
|
376
|
+
}
|
|
377
|
+
function editorText(doc, field = "default") {
|
|
378
|
+
return elementText(doc.getXmlFragment(field));
|
|
379
|
+
}
|
|
380
|
+
export {
|
|
381
|
+
Editor,
|
|
382
|
+
SlashMenu,
|
|
383
|
+
Toolbar,
|
|
384
|
+
defaultSlashCommands,
|
|
385
|
+
editorText
|
|
386
|
+
};
|
|
387
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/editor.tsx","../src/slash.tsx","../src/icons.tsx","../src/toolbar.tsx","../src/text.ts"],"sourcesContent":["import { cn } from \"@junco/react\";\nimport Collaboration from \"@tiptap/extension-collaboration\";\nimport CollaborationCaret from \"@tiptap/extension-collaboration-caret\";\nimport Placeholder from \"@tiptap/extension-placeholder\";\nimport { type Editor as TiptapEditor, EditorContent, type Extensions, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport * as React from \"react\";\nimport type * as Y from \"yjs\";\nimport { SlashMenu, type SlashCommand } from \"./slash\";\nimport { Toolbar } from \"./toolbar\";\n\n/** Anything with a y-protocols Awareness: a Towhee room, a y-websocket or Hocuspocus provider. */\nexport interface AwarenessProvider {\n // biome-ignore lint/suspicious/noExplicitAny: y-protocols Awareness, typed loosely to keep it optional\n awareness: any;\n}\n\nexport interface EditorProps {\n /** Starting content (HTML or TipTap JSON) when not collaborating. Ignored with `doc`: the document is the content. */\n content?: string | Record<string, unknown>;\n /** A shared Y.Doc to edit together. */\n doc?: Y.Doc;\n /** The fragment in `doc` this editor owns. Default `\"default\"`. */\n field?: string;\n /** Shows everyone's carets and selections. Pass the room or provider that holds the Awareness. */\n provider?: AwarenessProvider;\n /** You, as others see your caret. Towhee fills this in from the token, so it's optional there. */\n user?: { name: string; color?: string };\n placeholder?: string;\n editable?: boolean;\n /** Default true. */\n toolbar?: boolean;\n /** Commands for the \"/\" menu. Default: text, headings, lists, quote, code, divider. Pass [] to turn it off. */\n slashCommands?: SlashCommand[];\n /** Extra TipTap extensions. */\n extensions?: Extensions;\n onReady?: (editor: TiptapEditor) => void;\n onUpdate?: (editor: TiptapEditor) => void;\n /** Accessible name for the editing area. Default \"Document\". */\n label?: string;\n className?: string;\n classNames?: { root?: string; toolbar?: string; content?: string };\n}\n\n/**\n * Typography for the editing area, from Junco's tokens. Carets from other people get a neutral\n * name tag with a border in their color, so the text keeps its contrast for any color.\n */\nconst proseClasses = cn(\n \"min-h-48 px-4 py-3 text-[15px] leading-7 text-foreground outline-none\",\n \"[&_.ProseMirror]:min-h-44 [&_.ProseMirror]:outline-none [&_.ProseMirror>*+*]:mt-3\",\n \"[&_h1]:text-2xl [&_h1]:font-semibold [&_h1]:tracking-tight [&_h2]:text-xl [&_h2]:font-semibold [&_h3]:text-lg [&_h3]:font-semibold\",\n \"[&_ul]:list-disc [&_ul]:pl-6 [&_ol]:list-decimal [&_ol]:pl-6 [&_li>p]:my-0\",\n \"[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground\",\n \"[&_code]:rounded-xs [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em]\",\n \"[&_pre]:rounded-card [&_pre]:border [&_pre]:border-border [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:font-mono [&_pre]:text-sm [&_pre_code]:bg-transparent [&_pre_code]:p-0\",\n \"[&_hr]:my-6 [&_hr]:border-border [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4\",\n \"[&_p.is-editor-empty:first-child]:before:pointer-events-none [&_p.is-editor-empty:first-child]:before:float-left [&_p.is-editor-empty:first-child]:before:h-0 [&_p.is-editor-empty:first-child]:before:text-muted-foreground [&_p.is-editor-empty:first-child]:before:content-[attr(data-placeholder)]\",\n \"[&_.collaboration-carets\\\\_\\\\_caret]:relative [&_.collaboration-carets\\\\_\\\\_caret]:-mx-px [&_.collaboration-carets\\\\_\\\\_caret]:border-x [&_.collaboration-carets\\\\_\\\\_caret]:border-solid\",\n \"[&_.collaboration-carets\\\\_\\\\_label]:absolute [&_.collaboration-carets\\\\_\\\\_label]:-top-5 [&_.collaboration-carets\\\\_\\\\_label]:-left-px [&_.collaboration-carets\\\\_\\\\_label]:rounded-xs [&_.collaboration-carets\\\\_\\\\_label]:border-2 [&_.collaboration-carets\\\\_\\\\_label]:bg-popover [&_.collaboration-carets\\\\_\\\\_label]:px-1.5 [&_.collaboration-carets\\\\_\\\\_label]:text-[11px] [&_.collaboration-carets\\\\_\\\\_label]:leading-4 [&_.collaboration-carets\\\\_\\\\_label]:font-medium [&_.collaboration-carets\\\\_\\\\_label]:whitespace-nowrap [&_.collaboration-carets\\\\_\\\\_label]:text-popover-foreground [&_.collaboration-carets\\\\_\\\\_label]:select-none\",\n);\n\nfunction caret(user: Record<string, unknown>): HTMLElement {\n const color = typeof user.color === \"string\" ? user.color : \"var(--primary)\";\n const cursor = document.createElement(\"span\");\n cursor.className = \"collaboration-carets__caret\";\n cursor.style.borderColor = color;\n const label = document.createElement(\"span\");\n label.className = \"collaboration-carets__label\";\n label.style.borderColor = color;\n label.textContent = typeof user.name === \"string\" ? user.name : \"Someone\";\n cursor.append(label);\n return cursor;\n}\n\n/**\n * A rich text editor: bold, italic, headings, lists, quotes, code, links, markdown shortcuts and a\n * \"/\" command menu. Give it a Y.Doc (and the room that holds its Awareness) and everyone edits\n * the same document with their carets showing.\n */\nexport function Editor({\n content,\n doc,\n field = \"default\",\n provider,\n user,\n placeholder = \"Write something, or press / for commands\",\n editable = true,\n toolbar = true,\n slashCommands,\n extensions = [],\n onReady,\n onUpdate,\n label = \"Document\",\n className,\n classNames,\n}: EditorProps) {\n const callbacks = React.useRef({ onReady, onUpdate });\n callbacks.current = { onReady, onUpdate };\n\n const editor = useEditor(\n {\n immediatelyRender: false,\n editable,\n content: doc ? undefined : content,\n extensions: [\n // Yjs brings its own undo, which only undoes your changes; ProseMirror's would undo everyone's.\n StarterKit.configure({ undoRedo: doc ? false : undefined, link: { openOnClick: false } }),\n Placeholder.configure({ placeholder }),\n ...(doc ? [Collaboration.configure({ document: doc, field })] : []),\n ...(doc && provider ? [CollaborationCaret.configure({ provider, user: user ?? {}, render: caret })] : []),\n ...extensions,\n ],\n editorProps: { attributes: { \"aria-label\": label, \"aria-multiline\": \"true\", role: \"textbox\" } },\n onCreate: ({ editor: e }) => callbacks.current.onReady?.(e),\n onUpdate: ({ editor: e }) => callbacks.current.onUpdate?.(e),\n },\n [doc, field, provider],\n );\n\n React.useEffect(() => {\n editor?.setEditable(editable);\n }, [editor, editable]);\n\n return (\n <div className={cn(\"grid overflow-hidden rounded-card border border-border bg-surface\", className, classNames?.root)}>\n {toolbar && editor && editable && <Toolbar editor={editor} className={classNames?.toolbar} />}\n <EditorContent editor={editor} className={cn(proseClasses, classNames?.content)} />\n {editor && editable && <SlashMenu editor={editor} commands={slashCommands} />}\n </div>\n );\n}\n","import { cn } from \"@junco/react\";\nimport type { Editor } from \"@tiptap/react\";\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { CodeBlockIcon, ListIcon, ListOrderedIcon, MinusIcon, QuoteIcon, TextIcon } from \"./icons\";\n\nexport interface SlashCommand {\n id: string;\n label: string;\n /** Extra words to match while typing. */\n keywords?: string;\n icon?: React.ReactNode;\n /** Runs after the \"/query\" text is removed. */\n run: (editor: Editor) => void;\n}\n\nconst heading = (level: 1 | 2 | 3): SlashCommand => ({\n id: `h${level}`,\n label: `Heading ${level}`,\n keywords: `title h${level}`,\n icon: <span className=\"text-xs font-semibold\">H{level}</span>,\n run: (e) => e.chain().focus().setHeading({ level }).run(),\n});\n\nexport const defaultSlashCommands: SlashCommand[] = [\n { id: \"text\", label: \"Text\", keywords: \"paragraph plain\", icon: <TextIcon className=\"size-4\" />, run: (e) => e.chain().focus().setParagraph().run() },\n heading(1),\n heading(2),\n heading(3),\n { id: \"bullet\", label: \"Bulleted list\", keywords: \"ul unordered\", icon: <ListIcon className=\"size-4\" />, run: (e) => e.chain().focus().toggleBulletList().run() },\n { id: \"ordered\", label: \"Numbered list\", keywords: \"ol ordered\", icon: <ListOrderedIcon className=\"size-4\" />, run: (e) => e.chain().focus().toggleOrderedList().run() },\n { id: \"quote\", label: \"Quote\", keywords: \"blockquote\", icon: <QuoteIcon className=\"size-4\" />, run: (e) => e.chain().focus().setBlockquote().run() },\n { id: \"code\", label: \"Code block\", keywords: \"pre snippet\", icon: <CodeBlockIcon className=\"size-4\" />, run: (e) => e.chain().focus().setCodeBlock().run() },\n { id: \"divider\", label: \"Divider\", keywords: \"hr rule line\", icon: <MinusIcon className=\"size-4\" />, run: (e) => e.chain().focus().setHorizontalRule().run() },\n];\n\ninterface Open {\n from: number;\n to: number;\n query: string;\n left: number;\n top: number;\n}\n\n/** \"/\" at the start of a line or after a space, then letters: the open command and where it is. */\nfunction findSlash(editor: Editor): Omit<Open, \"left\" | \"top\"> | null {\n const { state } = editor;\n const { $from, empty } = state.selection;\n if (!empty || $from.parent.type.spec.code) return null;\n const before = $from.parent.textBetween(0, $from.parentOffset, undefined, \"\");\n const match = /(?:^|\\s)\\/([\\w-]{0,24})$/.exec(before);\n if (!match) return null;\n return { from: $from.pos - match[1]!.length - 1, to: $from.pos, query: match[1]!.toLowerCase() };\n}\n\n/** The \"/\" command menu. Arrow keys move, Enter runs, Escape closes. Shown by `Editor`. */\nexport function SlashMenu({ editor, commands = defaultSlashCommands }: { editor: Editor; commands?: SlashCommand[] }) {\n const [open, setOpen] = React.useState<Open | null>(null);\n const [index, setIndex] = React.useState(0);\n const [dismissedAt, setDismissedAt] = React.useState<number | null>(null);\n const listId = React.useId();\n\n const matches = React.useMemo(\n () => (open ? commands.filter((c) => `${c.label} ${c.keywords ?? \"\"}`.toLowerCase().includes(open.query)) : []),\n [commands, open],\n );\n\n React.useEffect(() => {\n if (commands.length === 0) return;\n const update = () => {\n const found = findSlash(editor);\n if (!found || found.from === dismissedAt) {\n setOpen(null);\n return;\n }\n const at = editor.view.coordsAtPos(found.from);\n setOpen((prev) => {\n // A new \"/\" or a changed query starts at the first match; other transactions keep the highlight.\n if (!prev || prev.from !== found.from || prev.query !== found.query) setIndex(0);\n return { ...found, left: at.left, top: at.bottom + 6 };\n });\n };\n const close = () => setOpen(null);\n editor.on(\"transaction\", update);\n editor.on(\"blur\", close);\n return () => {\n editor.off(\"transaction\", update);\n editor.off(\"blur\", close);\n };\n }, [editor, commands.length, dismissedAt]);\n\n const choose = React.useCallback(\n (command: SlashCommand | undefined) => {\n if (!command || !open) return;\n editor.chain().focus().deleteRange({ from: open.from, to: open.to }).run();\n command.run(editor);\n setOpen(null);\n },\n [editor, open],\n );\n\n React.useEffect(() => {\n if (!open || matches.length === 0) return;\n const onKey = (e: KeyboardEvent) => {\n const handled = [\"ArrowDown\", \"ArrowUp\", \"Enter\", \"Tab\", \"Escape\"].includes(e.key);\n if (handled) {\n // The editor listens on the same element: keep the key from also moving the caret or splitting the line.\n e.preventDefault();\n e.stopImmediatePropagation();\n }\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n const step = e.key === \"ArrowDown\" ? 1 : -1;\n setIndex((i) => (i + step + matches.length) % matches.length);\n } else if (e.key === \"Enter\" || e.key === \"Tab\") {\n choose(matches[index]);\n } else if (e.key === \"Escape\") {\n setDismissedAt(open.from);\n setOpen(null);\n }\n };\n const dom = editor.view.dom;\n dom.addEventListener(\"keydown\", onKey, true);\n return () => dom.removeEventListener(\"keydown\", onKey, true);\n }, [editor, open, matches, index, choose]);\n\n // Typing a new \"/\" somewhere else brings the menu back.\n React.useEffect(() => {\n if (dismissedAt !== null && findSlash(editor)?.from !== dismissedAt) setDismissedAt(null);\n });\n\n React.useEffect(() => {\n editor.view.dom.setAttribute(\"aria-expanded\", String(Boolean(open && matches.length)));\n if (open && matches.length) {\n editor.view.dom.setAttribute(\"aria-controls\", listId);\n editor.view.dom.setAttribute(\"aria-activedescendant\", `${listId}-${matches[index]?.id}`);\n } else {\n editor.view.dom.removeAttribute(\"aria-activedescendant\");\n }\n }, [editor, open, matches, index, listId]);\n\n if (!open || matches.length === 0 || typeof document === \"undefined\") return null;\n return createPortal(\n <div\n id={listId}\n role=\"listbox\"\n aria-label=\"Commands\"\n className=\"fixed z-popover w-60 animate-pop-in overflow-hidden rounded-card border border-border bg-popover p-1 text-popover-foreground shadow-float\"\n style={{ left: open.left, top: open.top }}\n >\n {matches.map((c, i) => (\n <div\n key={c.id}\n id={`${listId}-${c.id}`}\n role=\"option\"\n tabIndex={-1}\n aria-selected={i === index}\n onMouseDown={(e) => {\n e.preventDefault();\n choose(c);\n }}\n onMouseEnter={() => setIndex(i)}\n className={cn(\"flex h-9 cursor-default items-center gap-2.5 rounded-control px-2 text-sm\", i === index && \"bg-accent text-accent-foreground\")}\n >\n <span className=\"grid size-6 place-items-center rounded-xs border border-border bg-surface text-muted-foreground\">{c.icon}</span>\n {c.label}\n </div>\n ))}\n </div>,\n document.body,\n );\n}\n","import type * as React from \"react\";\n\n/** Toolbar and menu icons. Paths from Lucide (ISC). Size them with className. */\nfunction make(paths: React.ReactNode) {\n return (props: React.SVGProps<SVGSVGElement>) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n focusable={false}\n {...props}\n >\n {paths}\n </svg>\n );\n}\n\nexport const BoldIcon = make(<path d=\"M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8\" />);\nexport const ItalicIcon = make(\n <>\n <line x1=\"19\" x2=\"10\" y1=\"4\" y2=\"4\" />\n <line x1=\"14\" x2=\"5\" y1=\"20\" y2=\"20\" />\n <line x1=\"15\" x2=\"9\" y1=\"4\" y2=\"20\" />\n </>,\n);\nexport const StrikeIcon = make(\n <>\n <path d=\"M16 4H9a3 3 0 0 0-2.83 4\" />\n <path d=\"M14 12a4 4 0 0 1 0 8H6\" />\n <line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\" />\n </>,\n);\nexport const CodeIcon = make(\n <>\n <path d=\"m16 18 6-6-6-6\" />\n <path d=\"m8 6-6 6 6 6\" />\n </>,\n);\nexport const ListIcon = make(<path d=\"M3 12h.01M3 18h.01M3 6h.01M8 12h13M8 18h13M8 6h13\" />);\nexport const ListOrderedIcon = make(<path d=\"M10 12h11M10 18h11M10 6h11M4 10h2M4 6h1v4M6 18H4c0-1 2-2 2-3s-1-1.5-2-1\" />);\nexport const QuoteIcon = make(\n <>\n <path d=\"M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z\" />\n <path d=\"M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z\" />\n </>,\n);\nexport const CodeBlockIcon = make(\n <>\n <path d=\"m10 9-3 3 3 3M14 15l3-3-3-3\" />\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n </>,\n);\nexport const MinusIcon = make(<path d=\"M5 12h14\" />);\nexport const TextIcon = make(<path d=\"M17 6.1H3M21 12.1H3M15.1 18H3\" />);\nexport const UndoIcon = make(\n <>\n <path d=\"M9 14 4 9l5-5\" />\n <path d=\"M4 9h10.5a5.5 5.5 0 0 1 0 11H11\" />\n </>,\n);\nexport const RedoIcon = make(\n <>\n <path d=\"m15 14 5-5-5-5\" />\n <path d=\"M20 9H9.5a5.5 5.5 0 0 0 0 11H13\" />\n </>,\n);\n","import { cn, Tooltip } from \"@junco/react\";\nimport { type Editor, useEditorState } from \"@tiptap/react\";\nimport type * as React from \"react\";\nimport { BoldIcon, CodeBlockIcon, CodeIcon, ItalicIcon, ListIcon, ListOrderedIcon, QuoteIcon, RedoIcon, StrikeIcon, UndoIcon } from \"./icons\";\n\nconst mod = typeof navigator !== \"undefined\" && /Mac|iPhone|iPad/.test(navigator.platform) ? \"⌘\" : \"Ctrl+\";\n\nfunction Tool({ label, shortcut, active, disabled, onClick, children }: { label: string; shortcut?: string; active?: boolean; disabled?: boolean; onClick: () => void; children: React.ReactNode }) {\n return (\n <Tooltip content={shortcut ? `${label} (${shortcut})` : label}>\n <button\n type=\"button\"\n aria-label={label}\n aria-pressed={active === undefined ? undefined : active}\n disabled={disabled}\n // Keep the selection: a mousedown on the button would otherwise blur the editor first.\n onMouseDown={(e) => e.preventDefault()}\n onClick={onClick}\n className={cn(\n \"inline-grid size-8 place-items-center rounded-control text-muted-foreground transition-colors duration-fast ease-standard\",\n \"hover:bg-accent hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-40\",\n active && \"bg-accent text-foreground\",\n )}\n >\n {children}\n </button>\n </Tooltip>\n );\n}\n\nconst Divider = () => <span aria-hidden className=\"mx-1 h-5 w-px bg-border\" />;\n\n/** Formatting buttons that reflect the selection. Shown by `Editor`; use it on its own with any TipTap editor. */\nexport function Toolbar({ editor, className }: { editor: Editor; className?: string }) {\n const s = useEditorState({\n editor,\n selector: ({ editor: e }) => ({\n bold: e.isActive(\"bold\"),\n italic: e.isActive(\"italic\"),\n strike: e.isActive(\"strike\"),\n code: e.isActive(\"code\"),\n h1: e.isActive(\"heading\", { level: 1 }),\n h2: e.isActive(\"heading\", { level: 2 }),\n h3: e.isActive(\"heading\", { level: 3 }),\n bullet: e.isActive(\"bulletList\"),\n ordered: e.isActive(\"orderedList\"),\n quote: e.isActive(\"blockquote\"),\n codeBlock: e.isActive(\"codeBlock\"),\n canUndo: e.can().undo(),\n canRedo: e.can().redo(),\n }),\n });\n const run = (fn: (c: ReturnType<Editor[\"chain\"]>) => ReturnType<Editor[\"chain\"]>) => fn(editor.chain().focus()).run();\n return (\n <div role=\"toolbar\" aria-label=\"Formatting\" className={cn(\"flex flex-wrap items-center gap-0.5 border-b border-border bg-surface px-2 py-1.5\", className)}>\n <Tool label=\"Bold\" shortcut={`${mod}B`} active={s.bold} onClick={() => run((c) => c.toggleBold())}>\n <BoldIcon className=\"size-4\" />\n </Tool>\n <Tool label=\"Italic\" shortcut={`${mod}I`} active={s.italic} onClick={() => run((c) => c.toggleItalic())}>\n <ItalicIcon className=\"size-4\" />\n </Tool>\n <Tool label=\"Strikethrough\" active={s.strike} onClick={() => run((c) => c.toggleStrike())}>\n <StrikeIcon className=\"size-4\" />\n </Tool>\n <Tool label=\"Inline code\" active={s.code} onClick={() => run((c) => c.toggleCode())}>\n <CodeIcon className=\"size-4\" />\n </Tool>\n <Divider />\n {([1, 2, 3] as const).map((level) => (\n <Tool key={level} label={`Heading ${level}`} active={s[`h${level}`]} onClick={() => run((c) => c.toggleHeading({ level }))}>\n <span className=\"text-xs font-semibold\">H{level}</span>\n </Tool>\n ))}\n <Divider />\n <Tool label=\"Bulleted list\" active={s.bullet} onClick={() => run((c) => c.toggleBulletList())}>\n <ListIcon className=\"size-4\" />\n </Tool>\n <Tool label=\"Numbered list\" active={s.ordered} onClick={() => run((c) => c.toggleOrderedList())}>\n <ListOrderedIcon className=\"size-4\" />\n </Tool>\n <Tool label=\"Quote\" active={s.quote} onClick={() => run((c) => c.toggleBlockquote())}>\n <QuoteIcon className=\"size-4\" />\n </Tool>\n <Tool label=\"Code block\" active={s.codeBlock} onClick={() => run((c) => c.toggleCodeBlock())}>\n <CodeBlockIcon className=\"size-4\" />\n </Tool>\n <Divider />\n <Tool label=\"Undo\" shortcut={`${mod}Z`} disabled={!s.canUndo} onClick={() => run((c) => c.undo())}>\n <UndoIcon className=\"size-4\" />\n </Tool>\n <Tool label=\"Redo\" shortcut={`${mod}Shift+Z`} disabled={!s.canRedo} onClick={() => run((c) => c.redo())}>\n <RedoIcon className=\"size-4\" />\n </Tool>\n </div>\n );\n}\n","import * as Y from \"yjs\";\n\n/** Text inside an element: inline text joined, child blocks on their own lines. */\nfunction elementText(el: Y.XmlElement | Y.XmlFragment): string {\n const children = el.toArray();\n const blocks = children.some((c) => c instanceof Y.XmlElement);\n return children\n .map((c) => (c instanceof Y.XmlText ? (c.toDelta() as { insert: unknown }[]).map((d) => (typeof d.insert === \"string\" ? d.insert : \"\")).join(\"\") : elementText(c as Y.XmlElement)))\n .join(blocks ? \"\\n\" : \"\");\n}\n\n/**\n * A collaborative editor's document as plain text, one block per line. Use it to compare versions:\n * `towheeAdapter({ room, textOf: (doc) => editorText(doc) })`.\n */\nexport function editorText(doc: Y.Doc, field = \"default\"): string {\n return elementText(doc.getXmlFragment(field));\n}\n"],"mappings":"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","names":["cn","React","jsx","jsxs","cn","jsx","jsxs","cn","jsx","jsxs","cn"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,79 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junco/editor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Junco rich text editor on TipTap: toolbar, slash commands, markdown shortcuts, and real-time collaboration with shared cursors on any Yjs document (Towhee or others).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Oluwatobi Olajide",
|
|
7
|
+
"homepage": "https://junco.madebytobi.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/tobibytes/junco.git",
|
|
11
|
+
"directory": "packages/editor"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/tobibytes/junco/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"editor",
|
|
18
|
+
"rich-text",
|
|
19
|
+
"tiptap",
|
|
20
|
+
"prosemirror",
|
|
21
|
+
"yjs",
|
|
22
|
+
"collaboration",
|
|
23
|
+
"react",
|
|
24
|
+
"tailwindcss"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@tiptap/core": "^3.32.1",
|
|
42
|
+
"@tiptap/extension-collaboration": "^3.32.1",
|
|
43
|
+
"@tiptap/extension-collaboration-caret": "^3.32.1",
|
|
44
|
+
"@tiptap/extension-placeholder": "^3.32.1",
|
|
45
|
+
"@tiptap/pm": "^3.32.1",
|
|
46
|
+
"@tiptap/react": "^3.32.1",
|
|
47
|
+
"@tiptap/starter-kit": "^3.32.1",
|
|
48
|
+
"@tiptap/y-tiptap": "^3.0.11",
|
|
49
|
+
"@junco/react": "0.2.3"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": "^18.3.0 || ^19.0.0",
|
|
53
|
+
"react-dom": "^18.3.0 || ^19.0.0",
|
|
54
|
+
"yjs": "^13.6.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
58
|
+
"@testing-library/react": "^16.3.3",
|
|
59
|
+
"@testing-library/user-event": "^14.6.8",
|
|
60
|
+
"@types/react": "^19.0.0",
|
|
61
|
+
"@types/react-dom": "^19.0.0",
|
|
62
|
+
"jsdom": "^30.1.3",
|
|
63
|
+
"react": "^19.0.0",
|
|
64
|
+
"react-dom": "^19.0.0",
|
|
65
|
+
"tsup": "^8.3.0",
|
|
66
|
+
"typescript": "^5.6.0",
|
|
67
|
+
"vitest": "^3",
|
|
68
|
+
"yjs": "^13.6.33"
|
|
69
|
+
},
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "tsup",
|
|
75
|
+
"dev": "tsup --watch",
|
|
76
|
+
"test": "vitest run",
|
|
77
|
+
"typecheck": "tsc --noEmit"
|
|
78
|
+
}
|
|
6
79
|
}
|