notra-editor 0.1.0 → 0.3.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/README.md +95 -0
- package/dist/index.cjs +1195 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -1
- package/dist/index.d.ts +81 -1
- package/dist/index.mjs +1182 -8
- package/dist/index.mjs.map +1 -1
- package/dist/styles/globals.css +1141 -0
- package/dist/themes/default/editor.css +238 -0
- package/package.json +17 -5
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,1035 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import "./styles/globals.css";
|
|
3
|
+
|
|
1
4
|
// src/notra-editor.tsx
|
|
2
5
|
import { EditorContent } from "@tiptap/react";
|
|
3
6
|
|
|
7
|
+
// src/components/blockquote-button/blockquote-button.tsx
|
|
8
|
+
import { TextQuote } from "lucide-react";
|
|
9
|
+
import { forwardRef, useCallback, useEffect, useState } from "react";
|
|
10
|
+
|
|
11
|
+
// src/components/ui/button.tsx
|
|
12
|
+
import { cva } from "class-variance-authority";
|
|
13
|
+
import { Slot } from "radix-ui";
|
|
14
|
+
|
|
15
|
+
// src/lib/utils.ts
|
|
16
|
+
import { clsx } from "clsx";
|
|
17
|
+
import { twMerge } from "tailwind-merge";
|
|
18
|
+
function cn(...inputs) {
|
|
19
|
+
return twMerge(clsx(inputs));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/components/ui/button.tsx
|
|
23
|
+
import { jsx } from "react/jsx-runtime";
|
|
24
|
+
var buttonVariants = cva(
|
|
25
|
+
"nt:group/button nt:inline-flex nt:shrink-0 nt:items-center nt:justify-center nt:rounded-lg nt:border nt:border-transparent nt:bg-clip-padding nt:text-sm nt:font-medium nt:whitespace-nowrap nt:transition-all nt:outline-none nt:select-none nt:focus-visible:border-ring nt:focus-visible:ring-3 nt:focus-visible:ring-ring/50 nt:active:not-aria-[haspopup]:translate-y-px nt:disabled:pointer-events-none nt:disabled:opacity-50 nt:aria-invalid:border-destructive nt:aria-invalid:ring-3 nt:aria-invalid:ring-destructive/20 nt:dark:aria-invalid:border-destructive/50 nt:dark:aria-invalid:ring-destructive/40 nt:[&_svg]:pointer-events-none nt:[&_svg]:shrink-0 nt:[&_svg:not([class*=size-])]:size-4",
|
|
26
|
+
{
|
|
27
|
+
variants: {
|
|
28
|
+
variant: {
|
|
29
|
+
default: "nt:bg-primary nt:text-primary-foreground nt:[a]:hover:bg-primary/80",
|
|
30
|
+
outline: "nt:border-border nt:bg-background nt:hover:bg-muted nt:hover:text-foreground nt:aria-expanded:bg-muted nt:aria-expanded:text-foreground nt:dark:border-input nt:dark:bg-input/30 nt:dark:hover:bg-input/50",
|
|
31
|
+
secondary: "nt:bg-secondary nt:text-secondary-foreground nt:hover:bg-secondary/80 nt:aria-expanded:bg-secondary nt:aria-expanded:text-secondary-foreground",
|
|
32
|
+
ghost: "nt:hover:bg-muted nt:hover:text-foreground nt:aria-expanded:bg-muted nt:aria-expanded:text-foreground nt:dark:hover:bg-muted/50",
|
|
33
|
+
destructive: "nt:bg-destructive/10 nt:text-destructive nt:hover:bg-destructive/20 nt:focus-visible:border-destructive/40 nt:focus-visible:ring-destructive/20 nt:dark:bg-destructive/20 nt:dark:hover:bg-destructive/30 nt:dark:focus-visible:ring-destructive/40",
|
|
34
|
+
link: "nt:text-primary nt:underline-offset-4 nt:hover:underline"
|
|
35
|
+
},
|
|
36
|
+
size: {
|
|
37
|
+
default: "nt:h-8 nt:gap-1.5 nt:px-2.5 nt:has-data-[icon=inline-end]:pr-2 nt:has-data-[icon=inline-start]:pl-2",
|
|
38
|
+
xs: "nt:h-6 nt:gap-1 nt:rounded-[min(var(--radius-md),10px)] nt:px-2 nt:text-xs nt:in-data-[slot=button-group]:rounded-lg nt:has-data-[icon=inline-end]:pr-1.5 nt:has-data-[icon=inline-start]:pl-1.5 nt:[&_svg:not([class*=size-])]:size-3",
|
|
39
|
+
sm: "nt:h-7 nt:gap-1 nt:rounded-[min(var(--radius-md),12px)] nt:px-2.5 nt:text-[0.8rem] nt:in-data-[slot=button-group]:rounded-lg nt:has-data-[icon=inline-end]:pr-1.5 nt:has-data-[icon=inline-start]:pl-1.5 nt:[&_svg:not([class*=size-])]:size-3.5",
|
|
40
|
+
lg: "nt:h-9 nt:gap-1.5 nt:px-2.5 nt:has-data-[icon=inline-end]:pr-2 nt:has-data-[icon=inline-start]:pl-2",
|
|
41
|
+
icon: "nt:size-8",
|
|
42
|
+
"icon-xs": "nt:size-6 nt:rounded-[min(var(--radius-md),10px)] nt:in-data-[slot=button-group]:rounded-lg nt:[&_svg:not([class*=size-])]:size-3",
|
|
43
|
+
"icon-sm": "nt:size-7 nt:rounded-[min(var(--radius-md),12px)] nt:in-data-[slot=button-group]:rounded-lg",
|
|
44
|
+
"icon-lg": "nt:size-9"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
defaultVariants: {
|
|
48
|
+
variant: "default",
|
|
49
|
+
size: "default"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
function Button({
|
|
54
|
+
className,
|
|
55
|
+
variant = "default",
|
|
56
|
+
size = "default",
|
|
57
|
+
asChild = false,
|
|
58
|
+
...props
|
|
59
|
+
}) {
|
|
60
|
+
const Comp = asChild ? Slot.Root : "button";
|
|
61
|
+
return /* @__PURE__ */ jsx(
|
|
62
|
+
Comp,
|
|
63
|
+
{
|
|
64
|
+
className: cn(buttonVariants({ variant, size, className })),
|
|
65
|
+
"data-size": size,
|
|
66
|
+
"data-slot": "button",
|
|
67
|
+
"data-variant": variant,
|
|
68
|
+
...props
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/components/blockquote-button/blockquote-button.tsx
|
|
74
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
75
|
+
function canToggleBlockquote(editor) {
|
|
76
|
+
if (!editor || !editor.isEditable) return false;
|
|
77
|
+
return editor.can().toggleWrap("blockquote") || editor.can().clearNodes();
|
|
78
|
+
}
|
|
79
|
+
var BlockquoteButton = forwardRef(({ editor, onClick, ...buttonProps }, ref) => {
|
|
80
|
+
const [isActive, setIsActive] = useState(false);
|
|
81
|
+
const [canToggle, setCanToggle] = useState(false);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!editor) return;
|
|
84
|
+
const update = () => {
|
|
85
|
+
setIsActive(editor.isActive("blockquote"));
|
|
86
|
+
setCanToggle(canToggleBlockquote(editor));
|
|
87
|
+
};
|
|
88
|
+
update();
|
|
89
|
+
editor.on("selectionUpdate", update);
|
|
90
|
+
editor.on("transaction", update);
|
|
91
|
+
return () => {
|
|
92
|
+
editor.off("selectionUpdate", update);
|
|
93
|
+
editor.off("transaction", update);
|
|
94
|
+
};
|
|
95
|
+
}, [editor]);
|
|
96
|
+
const handleClick = useCallback(
|
|
97
|
+
(event) => {
|
|
98
|
+
onClick?.(event);
|
|
99
|
+
if (event.defaultPrevented) return;
|
|
100
|
+
if (!editor) return;
|
|
101
|
+
if (editor.isActive("blockquote")) {
|
|
102
|
+
editor.chain().focus().lift("blockquote").run();
|
|
103
|
+
} else {
|
|
104
|
+
editor.chain().focus().clearNodes().wrapIn("blockquote").run();
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[editor, onClick]
|
|
108
|
+
);
|
|
109
|
+
return /* @__PURE__ */ jsx2(
|
|
110
|
+
Button,
|
|
111
|
+
{
|
|
112
|
+
ref,
|
|
113
|
+
"aria-label": "Blockquote",
|
|
114
|
+
"aria-pressed": isActive,
|
|
115
|
+
"data-active-state": isActive ? "on" : "off",
|
|
116
|
+
disabled: !canToggle,
|
|
117
|
+
size: "icon",
|
|
118
|
+
tabIndex: -1,
|
|
119
|
+
type: "button",
|
|
120
|
+
variant: "ghost",
|
|
121
|
+
onClick: handleClick,
|
|
122
|
+
...buttonProps,
|
|
123
|
+
children: /* @__PURE__ */ jsx2(
|
|
124
|
+
TextQuote,
|
|
125
|
+
{
|
|
126
|
+
className: isActive ? "nt:text-[var(--tt-brand-color-500)]" : void 0
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
BlockquoteButton.displayName = "BlockquoteButton";
|
|
133
|
+
|
|
134
|
+
// src/components/code-block-button/code-block-button.tsx
|
|
135
|
+
import { SquareCode } from "lucide-react";
|
|
136
|
+
import { forwardRef as forwardRef2, useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
|
|
137
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
138
|
+
function canToggleCodeBlock(editor) {
|
|
139
|
+
if (!editor || !editor.isEditable) return false;
|
|
140
|
+
return editor.can().toggleNode("codeBlock", "paragraph") || editor.can().clearNodes();
|
|
141
|
+
}
|
|
142
|
+
var CodeBlockButton = forwardRef2(({ editor, onClick, ...buttonProps }, ref) => {
|
|
143
|
+
const [isActive, setIsActive] = useState2(false);
|
|
144
|
+
const [canToggle, setCanToggle] = useState2(false);
|
|
145
|
+
useEffect2(() => {
|
|
146
|
+
if (!editor) return;
|
|
147
|
+
const update = () => {
|
|
148
|
+
setIsActive(editor.isActive("codeBlock"));
|
|
149
|
+
setCanToggle(canToggleCodeBlock(editor));
|
|
150
|
+
};
|
|
151
|
+
update();
|
|
152
|
+
editor.on("selectionUpdate", update);
|
|
153
|
+
editor.on("transaction", update);
|
|
154
|
+
return () => {
|
|
155
|
+
editor.off("selectionUpdate", update);
|
|
156
|
+
editor.off("transaction", update);
|
|
157
|
+
};
|
|
158
|
+
}, [editor]);
|
|
159
|
+
const handleClick = useCallback2(
|
|
160
|
+
(event) => {
|
|
161
|
+
onClick?.(event);
|
|
162
|
+
if (event.defaultPrevented) return;
|
|
163
|
+
if (!editor) return;
|
|
164
|
+
if (editor.isActive("codeBlock")) {
|
|
165
|
+
editor.chain().focus().setNode("paragraph").run();
|
|
166
|
+
} else {
|
|
167
|
+
editor.chain().focus().clearNodes().toggleNode("codeBlock", "paragraph").run();
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
[editor, onClick]
|
|
171
|
+
);
|
|
172
|
+
return /* @__PURE__ */ jsx3(
|
|
173
|
+
Button,
|
|
174
|
+
{
|
|
175
|
+
ref,
|
|
176
|
+
"aria-label": "Code Block",
|
|
177
|
+
"aria-pressed": isActive,
|
|
178
|
+
"data-active-state": isActive ? "on" : "off",
|
|
179
|
+
disabled: !canToggle,
|
|
180
|
+
size: "icon",
|
|
181
|
+
tabIndex: -1,
|
|
182
|
+
type: "button",
|
|
183
|
+
variant: "ghost",
|
|
184
|
+
onClick: handleClick,
|
|
185
|
+
...buttonProps,
|
|
186
|
+
children: /* @__PURE__ */ jsx3(
|
|
187
|
+
SquareCode,
|
|
188
|
+
{
|
|
189
|
+
className: isActive ? "nt:text-[var(--tt-brand-color-500)]" : void 0
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
CodeBlockButton.displayName = "CodeBlockButton";
|
|
196
|
+
|
|
197
|
+
// src/components/heading-dropdown-menu/heading-dropdown-menu.tsx
|
|
198
|
+
import { ChevronDown } from "lucide-react";
|
|
199
|
+
import { forwardRef as forwardRef4 } from "react";
|
|
200
|
+
|
|
201
|
+
// src/components/heading-dropdown-menu/heading-menu-item.tsx
|
|
202
|
+
import { forwardRef as forwardRef3 } from "react";
|
|
203
|
+
|
|
204
|
+
// src/components/heading-dropdown-menu/use-heading.ts
|
|
205
|
+
import { Heading, Heading1, Heading2, Heading3, Heading4 } from "lucide-react";
|
|
206
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useState as useState3 } from "react";
|
|
207
|
+
var headingIcons = {
|
|
208
|
+
1: Heading1,
|
|
209
|
+
2: Heading2,
|
|
210
|
+
3: Heading3,
|
|
211
|
+
4: Heading4
|
|
212
|
+
};
|
|
213
|
+
var headingLabels = {
|
|
214
|
+
1: "Heading 1",
|
|
215
|
+
2: "Heading 2",
|
|
216
|
+
3: "Heading 3",
|
|
217
|
+
4: "Heading 4"
|
|
218
|
+
};
|
|
219
|
+
function canToggleHeading(editor, level) {
|
|
220
|
+
if (!editor || !editor.isEditable) return false;
|
|
221
|
+
return editor.can().setNode("heading", { level }) || editor.can().clearNodes();
|
|
222
|
+
}
|
|
223
|
+
function useHeading({
|
|
224
|
+
editor,
|
|
225
|
+
level
|
|
226
|
+
}) {
|
|
227
|
+
const [isActive, setIsActive] = useState3(false);
|
|
228
|
+
const [canToggle, setCanToggle] = useState3(false);
|
|
229
|
+
useEffect3(() => {
|
|
230
|
+
if (!editor) return;
|
|
231
|
+
const handleUpdate = () => {
|
|
232
|
+
setIsActive(editor.isActive("heading", { level }));
|
|
233
|
+
setCanToggle(canToggleHeading(editor, level));
|
|
234
|
+
};
|
|
235
|
+
handleUpdate();
|
|
236
|
+
editor.on("selectionUpdate", handleUpdate);
|
|
237
|
+
editor.on("transaction", handleUpdate);
|
|
238
|
+
return () => {
|
|
239
|
+
editor.off("selectionUpdate", handleUpdate);
|
|
240
|
+
editor.off("transaction", handleUpdate);
|
|
241
|
+
};
|
|
242
|
+
}, [editor, level]);
|
|
243
|
+
const handleToggle = useCallback3(() => {
|
|
244
|
+
if (!editor || !editor.isEditable) return false;
|
|
245
|
+
if (editor.isActive("heading", { level })) {
|
|
246
|
+
return editor.chain().focus().setNode("paragraph").run();
|
|
247
|
+
}
|
|
248
|
+
return editor.chain().focus().clearNodes().setNode("heading", { level }).run();
|
|
249
|
+
}, [editor, level]);
|
|
250
|
+
return {
|
|
251
|
+
isActive,
|
|
252
|
+
canToggle,
|
|
253
|
+
handleToggle,
|
|
254
|
+
label: headingLabels[level],
|
|
255
|
+
Icon: headingIcons[level]
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function useActiveHeadingLevel(editor, levels) {
|
|
259
|
+
const [activeLevel, setActiveLevel] = useState3(null);
|
|
260
|
+
useEffect3(() => {
|
|
261
|
+
if (!editor) return;
|
|
262
|
+
const handleUpdate = () => {
|
|
263
|
+
const found = levels.find(
|
|
264
|
+
(level) => editor.isActive("heading", { level })
|
|
265
|
+
);
|
|
266
|
+
setActiveLevel(found ?? null);
|
|
267
|
+
};
|
|
268
|
+
handleUpdate();
|
|
269
|
+
editor.on("selectionUpdate", handleUpdate);
|
|
270
|
+
editor.on("transaction", handleUpdate);
|
|
271
|
+
return () => {
|
|
272
|
+
editor.off("selectionUpdate", handleUpdate);
|
|
273
|
+
editor.off("transaction", handleUpdate);
|
|
274
|
+
};
|
|
275
|
+
}, [editor, levels]);
|
|
276
|
+
return activeLevel;
|
|
277
|
+
}
|
|
278
|
+
function getHeadingTriggerIcon(activeLevel) {
|
|
279
|
+
if (activeLevel === null) return Heading;
|
|
280
|
+
return headingIcons[activeLevel];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/components/ui/dropdown-menu.tsx
|
|
284
|
+
import { CheckIcon, ChevronRightIcon } from "lucide-react";
|
|
285
|
+
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
|
286
|
+
import { jsx as jsx4, jsxs } from "react/jsx-runtime";
|
|
287
|
+
function DropdownMenu({
|
|
288
|
+
...props
|
|
289
|
+
}) {
|
|
290
|
+
return /* @__PURE__ */ jsx4(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
|
|
291
|
+
}
|
|
292
|
+
function DropdownMenuTrigger({
|
|
293
|
+
...props
|
|
294
|
+
}) {
|
|
295
|
+
return /* @__PURE__ */ jsx4(
|
|
296
|
+
DropdownMenuPrimitive.Trigger,
|
|
297
|
+
{
|
|
298
|
+
"data-slot": "dropdown-menu-trigger",
|
|
299
|
+
...props
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
function DropdownMenuContent({
|
|
304
|
+
className,
|
|
305
|
+
align = "start",
|
|
306
|
+
sideOffset = 4,
|
|
307
|
+
...props
|
|
308
|
+
}) {
|
|
309
|
+
return /* @__PURE__ */ jsx4(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx4(
|
|
310
|
+
DropdownMenuPrimitive.Content,
|
|
311
|
+
{
|
|
312
|
+
align,
|
|
313
|
+
className: cn(
|
|
314
|
+
"nt:z-50 nt:max-h-(--radix-dropdown-menu-content-available-height) nt:w-(--radix-dropdown-menu-trigger-width) nt:min-w-32 nt:origin-(--radix-dropdown-menu-content-transform-origin) nt:overflow-x-hidden nt:overflow-y-auto nt:rounded-lg nt:bg-popover nt:p-1 nt:text-popover-foreground nt:shadow-md nt:ring-1 nt:ring-foreground/10 nt:duration-100 nt:data-[side=bottom]:slide-in-from-top-2 nt:data-[side=left]:slide-in-from-right-2 nt:data-[side=right]:slide-in-from-left-2 nt:data-[side=top]:slide-in-from-bottom-2 nt:data-[state=closed]:overflow-hidden nt:data-open:animate-in nt:data-open:fade-in-0 nt:data-open:zoom-in-95 nt:data-closed:animate-out nt:data-closed:fade-out-0 nt:data-closed:zoom-out-95",
|
|
315
|
+
className
|
|
316
|
+
),
|
|
317
|
+
"data-slot": "dropdown-menu-content",
|
|
318
|
+
sideOffset,
|
|
319
|
+
...props
|
|
320
|
+
}
|
|
321
|
+
) });
|
|
322
|
+
}
|
|
323
|
+
function DropdownMenuGroup({
|
|
324
|
+
...props
|
|
325
|
+
}) {
|
|
326
|
+
return /* @__PURE__ */ jsx4(DropdownMenuPrimitive.Group, { "data-slot": "dropdown-menu-group", ...props });
|
|
327
|
+
}
|
|
328
|
+
function DropdownMenuItem({
|
|
329
|
+
className,
|
|
330
|
+
inset,
|
|
331
|
+
variant = "default",
|
|
332
|
+
...props
|
|
333
|
+
}) {
|
|
334
|
+
return /* @__PURE__ */ jsx4(
|
|
335
|
+
DropdownMenuPrimitive.Item,
|
|
336
|
+
{
|
|
337
|
+
className: cn(
|
|
338
|
+
"nt:group/dropdown-menu-item nt:relative nt:flex nt:cursor-default nt:items-center nt:gap-1.5 nt:rounded-md nt:px-1.5 nt:py-1 nt:text-sm nt:outline-hidden nt:select-none nt:focus:bg-accent nt:focus:text-accent-foreground nt:not-data-[variant=destructive]:focus:**:text-accent-foreground nt:data-inset:pl-7 nt:data-[variant=destructive]:text-destructive nt:data-[variant=destructive]:focus:bg-destructive/10 nt:data-[variant=destructive]:focus:text-destructive nt:dark:data-[variant=destructive]:focus:bg-destructive/20 nt:data-disabled:pointer-events-none nt:data-disabled:opacity-50 nt:[&_svg]:pointer-events-none nt:[&_svg]:shrink-0 nt:[&_svg:not([class*=size-])]:size-4 nt:data-[variant=destructive]:*:[svg]:text-destructive",
|
|
339
|
+
className
|
|
340
|
+
),
|
|
341
|
+
"data-inset": inset,
|
|
342
|
+
"data-slot": "dropdown-menu-item",
|
|
343
|
+
"data-variant": variant,
|
|
344
|
+
...props
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/components/heading-dropdown-menu/heading-menu-item.tsx
|
|
350
|
+
import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
351
|
+
var HeadingMenuItem = forwardRef3(
|
|
352
|
+
({ editor, level }, ref) => {
|
|
353
|
+
const { isActive, canToggle, handleToggle, label, Icon } = useHeading({
|
|
354
|
+
editor,
|
|
355
|
+
level
|
|
356
|
+
});
|
|
357
|
+
return /* @__PURE__ */ jsxs2(
|
|
358
|
+
DropdownMenuItem,
|
|
359
|
+
{
|
|
360
|
+
ref,
|
|
361
|
+
"aria-label": label,
|
|
362
|
+
className: "nt:data-[active-state=on]:bg-accent nt:data-[active-state=on]:text-[var(--tt-brand-color-500)]",
|
|
363
|
+
"data-active-state": isActive ? "on" : "off",
|
|
364
|
+
disabled: !canToggle,
|
|
365
|
+
onSelect: handleToggle,
|
|
366
|
+
children: [
|
|
367
|
+
/* @__PURE__ */ jsx5(Icon, {}),
|
|
368
|
+
/* @__PURE__ */ jsx5("span", { children: label })
|
|
369
|
+
]
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
HeadingMenuItem.displayName = "HeadingMenuItem";
|
|
375
|
+
|
|
376
|
+
// src/components/heading-dropdown-menu/heading-dropdown-menu.tsx
|
|
377
|
+
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
378
|
+
var HeadingDropdownMenu = forwardRef4(({ editor, levels = [1, 2, 3, 4], ...buttonProps }, ref) => {
|
|
379
|
+
const activeLevel = useActiveHeadingLevel(editor, levels);
|
|
380
|
+
const TriggerIcon = getHeadingTriggerIcon(activeLevel);
|
|
381
|
+
return /* @__PURE__ */ jsxs3(DropdownMenu, { children: [
|
|
382
|
+
/* @__PURE__ */ jsx6(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs3(
|
|
383
|
+
Button,
|
|
384
|
+
{
|
|
385
|
+
ref,
|
|
386
|
+
"aria-label": "Heading",
|
|
387
|
+
className: "nt:gap-1 nt:px-2",
|
|
388
|
+
"data-active-state": activeLevel !== null ? "on" : "off",
|
|
389
|
+
size: "default",
|
|
390
|
+
tabIndex: -1,
|
|
391
|
+
type: "button",
|
|
392
|
+
variant: "ghost",
|
|
393
|
+
...buttonProps,
|
|
394
|
+
children: [
|
|
395
|
+
/* @__PURE__ */ jsx6(
|
|
396
|
+
TriggerIcon,
|
|
397
|
+
{
|
|
398
|
+
className: activeLevel !== null ? "nt:text-[var(--tt-brand-color-500)]" : void 0
|
|
399
|
+
}
|
|
400
|
+
),
|
|
401
|
+
/* @__PURE__ */ jsx6(ChevronDown, { className: "nt:size-3" })
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
) }),
|
|
405
|
+
/* @__PURE__ */ jsx6(DropdownMenuContent, { align: "start", children: /* @__PURE__ */ jsx6(DropdownMenuGroup, { children: levels.map((level) => /* @__PURE__ */ jsx6(HeadingMenuItem, { editor, level }, level)) }) })
|
|
406
|
+
] });
|
|
407
|
+
});
|
|
408
|
+
HeadingDropdownMenu.displayName = "HeadingDropdownMenu";
|
|
409
|
+
|
|
410
|
+
// src/components/link-popover/link-popover.tsx
|
|
411
|
+
import {
|
|
412
|
+
CornerDownLeft,
|
|
413
|
+
ExternalLink,
|
|
414
|
+
Link as LinkIcon,
|
|
415
|
+
Trash2
|
|
416
|
+
} from "lucide-react";
|
|
417
|
+
import { forwardRef as forwardRef5, useCallback as useCallback5, useEffect as useEffect5, useState as useState5 } from "react";
|
|
418
|
+
|
|
419
|
+
// src/components/link-popover/use-link-popover.ts
|
|
420
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useState as useState4 } from "react";
|
|
421
|
+
function useLinkPopover({ editor }) {
|
|
422
|
+
const [url, setUrl] = useState4("");
|
|
423
|
+
const [isActive, setIsActive] = useState4(false);
|
|
424
|
+
const [canSet, setCanSet] = useState4(false);
|
|
425
|
+
useEffect4(() => {
|
|
426
|
+
if (!editor) return;
|
|
427
|
+
const handleUpdate = () => {
|
|
428
|
+
const active = editor.isActive("link");
|
|
429
|
+
setIsActive(active);
|
|
430
|
+
setCanSet(editor.isEditable);
|
|
431
|
+
if (active) {
|
|
432
|
+
setUrl(editor.getAttributes("link").href ?? "");
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
handleUpdate();
|
|
436
|
+
editor.on("selectionUpdate", handleUpdate);
|
|
437
|
+
editor.on("transaction", handleUpdate);
|
|
438
|
+
return () => {
|
|
439
|
+
editor.off("selectionUpdate", handleUpdate);
|
|
440
|
+
editor.off("transaction", handleUpdate);
|
|
441
|
+
};
|
|
442
|
+
}, [editor]);
|
|
443
|
+
const setLink = useCallback4(() => {
|
|
444
|
+
if (!editor) return;
|
|
445
|
+
if (!url) {
|
|
446
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
450
|
+
}, [editor, url]);
|
|
451
|
+
const removeLink = useCallback4(() => {
|
|
452
|
+
if (!editor) return;
|
|
453
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
454
|
+
setUrl("");
|
|
455
|
+
}, [editor]);
|
|
456
|
+
const openLink = useCallback4(() => {
|
|
457
|
+
if (!url) return;
|
|
458
|
+
const sanitized = /^https?:\/\//i.test(url) ? url : `https://${url}`;
|
|
459
|
+
window.open(sanitized, "_blank", "noopener,noreferrer");
|
|
460
|
+
}, [url]);
|
|
461
|
+
return { url, setUrl, isActive, canSet, setLink, removeLink, openLink };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/components/ui/input.tsx
|
|
465
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
466
|
+
function Input({ className, type, ...props }) {
|
|
467
|
+
return /* @__PURE__ */ jsx7(
|
|
468
|
+
"input",
|
|
469
|
+
{
|
|
470
|
+
className: cn(
|
|
471
|
+
"nt:flex nt:h-9 nt:w-full nt:min-w-0 nt:rounded-md nt:border nt:border-input nt:bg-transparent nt:px-3 nt:py-1 nt:text-base nt:shadow-xs nt:transition-[color,box-shadow] nt:outline-none nt:file:inline-flex nt:file:h-7 nt:file:border-0 nt:file:bg-transparent nt:file:text-sm nt:file:font-medium nt:file:text-foreground nt:placeholder:text-muted-foreground nt:selection:bg-primary nt:selection:text-primary-foreground nt:dark:bg-input/30 nt:md:text-sm nt:focus-visible:border-ring nt:focus-visible:ring-3 nt:focus-visible:ring-ring/50 nt:aria-invalid:border-destructive nt:aria-invalid:ring-3 nt:aria-invalid:ring-destructive/20 nt:dark:aria-invalid:ring-destructive/40 nt:disabled:cursor-not-allowed nt:disabled:opacity-50",
|
|
472
|
+
className
|
|
473
|
+
),
|
|
474
|
+
"data-slot": "input",
|
|
475
|
+
type,
|
|
476
|
+
...props
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/components/ui/popover.tsx
|
|
482
|
+
import { Popover as PopoverPrimitive } from "radix-ui";
|
|
483
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
484
|
+
function Popover({
|
|
485
|
+
...props
|
|
486
|
+
}) {
|
|
487
|
+
return /* @__PURE__ */ jsx8(PopoverPrimitive.Root, { "data-slot": "popover", ...props });
|
|
488
|
+
}
|
|
489
|
+
function PopoverTrigger({
|
|
490
|
+
...props
|
|
491
|
+
}) {
|
|
492
|
+
return /* @__PURE__ */ jsx8(PopoverPrimitive.Trigger, { "data-slot": "popover-trigger", ...props });
|
|
493
|
+
}
|
|
494
|
+
function PopoverContent({
|
|
495
|
+
className,
|
|
496
|
+
align = "center",
|
|
497
|
+
sideOffset = 4,
|
|
498
|
+
...props
|
|
499
|
+
}) {
|
|
500
|
+
return /* @__PURE__ */ jsx8(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx8(
|
|
501
|
+
PopoverPrimitive.Content,
|
|
502
|
+
{
|
|
503
|
+
align,
|
|
504
|
+
className: cn(
|
|
505
|
+
"nt:z-50 nt:w-72 nt:origin-(--radix-popover-content-transform-origin) nt:rounded-lg nt:bg-popover nt:p-4 nt:text-popover-foreground nt:shadow-md nt:ring-1 nt:ring-foreground/10 nt:outline-none nt:data-[side=bottom]:slide-in-from-top-2 nt:data-[side=left]:slide-in-from-right-2 nt:data-[side=right]:slide-in-from-left-2 nt:data-[side=top]:slide-in-from-bottom-2 nt:data-open:animate-in nt:data-open:fade-in-0 nt:data-open:zoom-in-95 nt:data-closed:animate-out nt:data-closed:fade-out-0 nt:data-closed:zoom-out-95",
|
|
506
|
+
className
|
|
507
|
+
),
|
|
508
|
+
"data-slot": "popover-content",
|
|
509
|
+
sideOffset,
|
|
510
|
+
...props
|
|
511
|
+
}
|
|
512
|
+
) });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/components/ui/separator.tsx
|
|
516
|
+
import { Separator as SeparatorPrimitive } from "radix-ui";
|
|
517
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
518
|
+
function Separator({
|
|
519
|
+
className,
|
|
520
|
+
orientation = "horizontal",
|
|
521
|
+
decorative = true,
|
|
522
|
+
...props
|
|
523
|
+
}) {
|
|
524
|
+
return /* @__PURE__ */ jsx9(
|
|
525
|
+
SeparatorPrimitive.Root,
|
|
526
|
+
{
|
|
527
|
+
className: cn(
|
|
528
|
+
"nt:shrink-0 nt:bg-border nt:data-[orientation=horizontal]:h-px nt:data-[orientation=horizontal]:w-full nt:data-[orientation=vertical]:h-full nt:data-[orientation=vertical]:w-px",
|
|
529
|
+
className
|
|
530
|
+
),
|
|
531
|
+
"data-slot": "separator",
|
|
532
|
+
decorative,
|
|
533
|
+
orientation,
|
|
534
|
+
...props
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/components/link-popover/link-popover.tsx
|
|
540
|
+
import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
541
|
+
var LinkPopover = forwardRef5(
|
|
542
|
+
({ editor, ...buttonProps }, ref) => {
|
|
543
|
+
const [isOpen, setIsOpen] = useState5(false);
|
|
544
|
+
const { url, setUrl, isActive, canSet, setLink, removeLink, openLink } = useLinkPopover({ editor });
|
|
545
|
+
useEffect5(() => {
|
|
546
|
+
if (isActive) {
|
|
547
|
+
setIsOpen(true);
|
|
548
|
+
}
|
|
549
|
+
}, [isActive]);
|
|
550
|
+
const handleSetLink = useCallback5(() => {
|
|
551
|
+
setLink();
|
|
552
|
+
setIsOpen(false);
|
|
553
|
+
}, [setLink]);
|
|
554
|
+
const handleRemoveLink = useCallback5(() => {
|
|
555
|
+
removeLink();
|
|
556
|
+
setIsOpen(false);
|
|
557
|
+
}, [removeLink]);
|
|
558
|
+
const handleKeyDown = useCallback5(
|
|
559
|
+
(event) => {
|
|
560
|
+
if (event.key === "Enter") {
|
|
561
|
+
event.preventDefault();
|
|
562
|
+
handleSetLink();
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
[handleSetLink]
|
|
566
|
+
);
|
|
567
|
+
return /* @__PURE__ */ jsxs4(Popover, { open: isOpen, onOpenChange: setIsOpen, children: [
|
|
568
|
+
/* @__PURE__ */ jsx10(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx10(
|
|
569
|
+
Button,
|
|
570
|
+
{
|
|
571
|
+
ref,
|
|
572
|
+
"aria-label": "Link",
|
|
573
|
+
"aria-pressed": isActive,
|
|
574
|
+
"data-active-state": isActive ? "on" : "off",
|
|
575
|
+
disabled: !canSet,
|
|
576
|
+
size: "icon",
|
|
577
|
+
tabIndex: -1,
|
|
578
|
+
type: "button",
|
|
579
|
+
variant: "ghost",
|
|
580
|
+
...buttonProps,
|
|
581
|
+
children: /* @__PURE__ */ jsx10(
|
|
582
|
+
LinkIcon,
|
|
583
|
+
{
|
|
584
|
+
className: isActive ? "nt:text-[var(--tt-brand-color-500)]" : void 0
|
|
585
|
+
}
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
) }),
|
|
589
|
+
/* @__PURE__ */ jsxs4(
|
|
590
|
+
PopoverContent,
|
|
591
|
+
{
|
|
592
|
+
align: "start",
|
|
593
|
+
className: "nt:flex nt:w-auto nt:items-center nt:gap-1 nt:p-1",
|
|
594
|
+
children: [
|
|
595
|
+
/* @__PURE__ */ jsx10(
|
|
596
|
+
Input,
|
|
597
|
+
{
|
|
598
|
+
autoFocus: true,
|
|
599
|
+
className: "nt:h-7 nt:min-w-48 nt:border-none nt:shadow-none nt:focus-visible:ring-0",
|
|
600
|
+
placeholder: "Paste a link...",
|
|
601
|
+
type: "url",
|
|
602
|
+
value: url,
|
|
603
|
+
onChange: (e) => setUrl(e.target.value),
|
|
604
|
+
onKeyDown: handleKeyDown
|
|
605
|
+
}
|
|
606
|
+
),
|
|
607
|
+
/* @__PURE__ */ jsx10(
|
|
608
|
+
Button,
|
|
609
|
+
{
|
|
610
|
+
"aria-label": "Apply link",
|
|
611
|
+
disabled: !url && !isActive,
|
|
612
|
+
size: "icon-sm",
|
|
613
|
+
tabIndex: -1,
|
|
614
|
+
type: "button",
|
|
615
|
+
variant: "ghost",
|
|
616
|
+
onClick: handleSetLink,
|
|
617
|
+
children: /* @__PURE__ */ jsx10(CornerDownLeft, {})
|
|
618
|
+
}
|
|
619
|
+
),
|
|
620
|
+
/* @__PURE__ */ jsx10(Separator, { className: "nt:h-5", orientation: "vertical" }),
|
|
621
|
+
/* @__PURE__ */ jsx10(
|
|
622
|
+
Button,
|
|
623
|
+
{
|
|
624
|
+
"aria-label": "Open link in new window",
|
|
625
|
+
size: "icon-sm",
|
|
626
|
+
tabIndex: -1,
|
|
627
|
+
type: "button",
|
|
628
|
+
variant: "ghost",
|
|
629
|
+
onClick: openLink,
|
|
630
|
+
children: /* @__PURE__ */ jsx10(ExternalLink, {})
|
|
631
|
+
}
|
|
632
|
+
),
|
|
633
|
+
/* @__PURE__ */ jsx10(
|
|
634
|
+
Button,
|
|
635
|
+
{
|
|
636
|
+
"aria-label": "Remove link",
|
|
637
|
+
size: "icon-sm",
|
|
638
|
+
tabIndex: -1,
|
|
639
|
+
type: "button",
|
|
640
|
+
variant: "ghost",
|
|
641
|
+
onClick: handleRemoveLink,
|
|
642
|
+
children: /* @__PURE__ */ jsx10(Trash2, {})
|
|
643
|
+
}
|
|
644
|
+
)
|
|
645
|
+
]
|
|
646
|
+
}
|
|
647
|
+
)
|
|
648
|
+
] });
|
|
649
|
+
}
|
|
650
|
+
);
|
|
651
|
+
LinkPopover.displayName = "LinkPopover";
|
|
652
|
+
|
|
653
|
+
// src/components/list-dropdown-menu/list-dropdown-menu.tsx
|
|
654
|
+
import { ChevronDown as ChevronDown2 } from "lucide-react";
|
|
655
|
+
import { forwardRef as forwardRef7 } from "react";
|
|
656
|
+
|
|
657
|
+
// src/components/list-dropdown-menu/list-menu-item.tsx
|
|
658
|
+
import { forwardRef as forwardRef6 } from "react";
|
|
659
|
+
|
|
660
|
+
// src/components/list-dropdown-menu/use-list.ts
|
|
661
|
+
import { List, ListOrdered, ListTodo } from "lucide-react";
|
|
662
|
+
import { useCallback as useCallback6, useEffect as useEffect6, useState as useState6 } from "react";
|
|
663
|
+
var listIcons = {
|
|
664
|
+
bulletList: List,
|
|
665
|
+
orderedList: ListOrdered,
|
|
666
|
+
taskList: ListTodo
|
|
667
|
+
};
|
|
668
|
+
var listLabels = {
|
|
669
|
+
bulletList: "Bullet List",
|
|
670
|
+
orderedList: "Ordered List",
|
|
671
|
+
taskList: "Task List"
|
|
672
|
+
};
|
|
673
|
+
var listItemTypes = {
|
|
674
|
+
bulletList: "listItem",
|
|
675
|
+
orderedList: "listItem",
|
|
676
|
+
taskList: "taskItem"
|
|
677
|
+
};
|
|
678
|
+
function canToggleList(editor) {
|
|
679
|
+
if (!editor || !editor.isEditable) return false;
|
|
680
|
+
return editor.can().toggleList("bulletList", "listItem") || editor.can().clearNodes();
|
|
681
|
+
}
|
|
682
|
+
function useList({
|
|
683
|
+
editor,
|
|
684
|
+
type
|
|
685
|
+
}) {
|
|
686
|
+
const [isActive, setIsActive] = useState6(false);
|
|
687
|
+
const [canToggle, setCanToggle] = useState6(false);
|
|
688
|
+
useEffect6(() => {
|
|
689
|
+
if (!editor) return;
|
|
690
|
+
const handleUpdate = () => {
|
|
691
|
+
setIsActive(editor.isActive(type));
|
|
692
|
+
setCanToggle(canToggleList(editor));
|
|
693
|
+
};
|
|
694
|
+
handleUpdate();
|
|
695
|
+
editor.on("selectionUpdate", handleUpdate);
|
|
696
|
+
editor.on("transaction", handleUpdate);
|
|
697
|
+
return () => {
|
|
698
|
+
editor.off("selectionUpdate", handleUpdate);
|
|
699
|
+
editor.off("transaction", handleUpdate);
|
|
700
|
+
};
|
|
701
|
+
}, [editor, type]);
|
|
702
|
+
const handleToggle = useCallback6(() => {
|
|
703
|
+
if (!editor || !editor.isEditable) return false;
|
|
704
|
+
const itemType = listItemTypes[type];
|
|
705
|
+
if (editor.isActive(type)) {
|
|
706
|
+
return editor.chain().focus().clearNodes().run();
|
|
707
|
+
}
|
|
708
|
+
return editor.chain().focus().clearNodes().toggleList(type, itemType).run();
|
|
709
|
+
}, [editor, type]);
|
|
710
|
+
return {
|
|
711
|
+
isActive,
|
|
712
|
+
canToggle,
|
|
713
|
+
handleToggle,
|
|
714
|
+
label: listLabels[type],
|
|
715
|
+
Icon: listIcons[type]
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function useActiveListType(editor, types) {
|
|
719
|
+
const [activeType, setActiveType] = useState6(null);
|
|
720
|
+
useEffect6(() => {
|
|
721
|
+
if (!editor) return;
|
|
722
|
+
const handleUpdate = () => {
|
|
723
|
+
const found = types.find((type) => editor.isActive(type));
|
|
724
|
+
setActiveType(found ?? null);
|
|
725
|
+
};
|
|
726
|
+
handleUpdate();
|
|
727
|
+
editor.on("selectionUpdate", handleUpdate);
|
|
728
|
+
editor.on("transaction", handleUpdate);
|
|
729
|
+
return () => {
|
|
730
|
+
editor.off("selectionUpdate", handleUpdate);
|
|
731
|
+
editor.off("transaction", handleUpdate);
|
|
732
|
+
};
|
|
733
|
+
}, [editor, types]);
|
|
734
|
+
return activeType;
|
|
735
|
+
}
|
|
736
|
+
function getListTriggerIcon(activeType) {
|
|
737
|
+
if (activeType === null) return List;
|
|
738
|
+
return listIcons[activeType];
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/components/list-dropdown-menu/list-menu-item.tsx
|
|
742
|
+
import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
743
|
+
var ListMenuItem = forwardRef6(
|
|
744
|
+
({ editor, listType }, ref) => {
|
|
745
|
+
const { isActive, canToggle, handleToggle, label, Icon } = useList({
|
|
746
|
+
editor,
|
|
747
|
+
type: listType
|
|
748
|
+
});
|
|
749
|
+
return /* @__PURE__ */ jsxs5(
|
|
750
|
+
DropdownMenuItem,
|
|
751
|
+
{
|
|
752
|
+
ref,
|
|
753
|
+
"aria-label": label,
|
|
754
|
+
className: "nt:data-[active-state=on]:bg-accent nt:data-[active-state=on]:text-[var(--tt-brand-color-500)]",
|
|
755
|
+
"data-active-state": isActive ? "on" : "off",
|
|
756
|
+
disabled: !canToggle,
|
|
757
|
+
onSelect: handleToggle,
|
|
758
|
+
children: [
|
|
759
|
+
/* @__PURE__ */ jsx11(Icon, {}),
|
|
760
|
+
/* @__PURE__ */ jsx11("span", { children: label })
|
|
761
|
+
]
|
|
762
|
+
}
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
);
|
|
766
|
+
ListMenuItem.displayName = "ListMenuItem";
|
|
767
|
+
|
|
768
|
+
// src/components/list-dropdown-menu/list-dropdown-menu.tsx
|
|
769
|
+
import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
770
|
+
var ListDropdownMenu = forwardRef7(
|
|
771
|
+
({
|
|
772
|
+
editor,
|
|
773
|
+
types = ["bulletList", "orderedList", "taskList"],
|
|
774
|
+
...buttonProps
|
|
775
|
+
}, ref) => {
|
|
776
|
+
const activeType = useActiveListType(editor, types);
|
|
777
|
+
const TriggerIcon = getListTriggerIcon(activeType);
|
|
778
|
+
return /* @__PURE__ */ jsxs6(DropdownMenu, { children: [
|
|
779
|
+
/* @__PURE__ */ jsx12(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs6(
|
|
780
|
+
Button,
|
|
781
|
+
{
|
|
782
|
+
ref,
|
|
783
|
+
"aria-label": "List",
|
|
784
|
+
className: "nt:gap-1 nt:px-2",
|
|
785
|
+
"data-active-state": activeType !== null ? "on" : "off",
|
|
786
|
+
size: "default",
|
|
787
|
+
tabIndex: -1,
|
|
788
|
+
type: "button",
|
|
789
|
+
variant: "ghost",
|
|
790
|
+
...buttonProps,
|
|
791
|
+
children: [
|
|
792
|
+
/* @__PURE__ */ jsx12(
|
|
793
|
+
TriggerIcon,
|
|
794
|
+
{
|
|
795
|
+
className: activeType !== null ? "nt:text-[var(--tt-brand-color-500)]" : void 0
|
|
796
|
+
}
|
|
797
|
+
),
|
|
798
|
+
/* @__PURE__ */ jsx12(ChevronDown2, { className: "nt:size-3" })
|
|
799
|
+
]
|
|
800
|
+
}
|
|
801
|
+
) }),
|
|
802
|
+
/* @__PURE__ */ jsx12(DropdownMenuContent, { align: "start", children: /* @__PURE__ */ jsx12(DropdownMenuGroup, { children: types.map((type) => /* @__PURE__ */ jsx12(ListMenuItem, { editor, listType: type }, type)) }) })
|
|
803
|
+
] });
|
|
804
|
+
}
|
|
805
|
+
);
|
|
806
|
+
ListDropdownMenu.displayName = "ListDropdownMenu";
|
|
807
|
+
|
|
808
|
+
// src/components/mark-button/mark-button.tsx
|
|
809
|
+
import { forwardRef as forwardRef8, useCallback as useCallback8 } from "react";
|
|
810
|
+
|
|
811
|
+
// src/components/mark-button/use-mark.ts
|
|
812
|
+
import { Bold, Code, Italic, Strikethrough } from "lucide-react";
|
|
813
|
+
import { useCallback as useCallback7, useEffect as useEffect7, useState as useState7 } from "react";
|
|
814
|
+
var markLabels = {
|
|
815
|
+
bold: "Bold",
|
|
816
|
+
italic: "Italic",
|
|
817
|
+
strike: "Strikethrough",
|
|
818
|
+
code: "Code"
|
|
819
|
+
};
|
|
820
|
+
var markIcons = {
|
|
821
|
+
bold: Bold,
|
|
822
|
+
italic: Italic,
|
|
823
|
+
strike: Strikethrough,
|
|
824
|
+
code: Code
|
|
825
|
+
};
|
|
826
|
+
function useMark({ editor, type }) {
|
|
827
|
+
const [isActive, setIsActive] = useState7(false);
|
|
828
|
+
const [canToggle, setCanToggle] = useState7(false);
|
|
829
|
+
useEffect7(() => {
|
|
830
|
+
if (!editor) return;
|
|
831
|
+
const handleUpdate = () => {
|
|
832
|
+
setIsActive(editor.isActive(type));
|
|
833
|
+
setCanToggle(editor.isEditable && editor.can().toggleMark(type));
|
|
834
|
+
};
|
|
835
|
+
handleUpdate();
|
|
836
|
+
editor.on("selectionUpdate", handleUpdate);
|
|
837
|
+
editor.on("transaction", handleUpdate);
|
|
838
|
+
return () => {
|
|
839
|
+
editor.off("selectionUpdate", handleUpdate);
|
|
840
|
+
editor.off("transaction", handleUpdate);
|
|
841
|
+
};
|
|
842
|
+
}, [editor, type]);
|
|
843
|
+
const handleToggle = useCallback7(() => {
|
|
844
|
+
if (!editor || !editor.isEditable) return false;
|
|
845
|
+
return editor.chain().focus().toggleMark(type).run();
|
|
846
|
+
}, [editor, type]);
|
|
847
|
+
return {
|
|
848
|
+
isActive,
|
|
849
|
+
canToggle,
|
|
850
|
+
handleToggle,
|
|
851
|
+
label: markLabels[type],
|
|
852
|
+
Icon: markIcons[type]
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/components/mark-button/mark-button.tsx
|
|
857
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
858
|
+
var MarkButton = forwardRef8(
|
|
859
|
+
({ editor, type, onClick, ...buttonProps }, ref) => {
|
|
860
|
+
const { isActive, canToggle, handleToggle, label, Icon } = useMark({
|
|
861
|
+
editor,
|
|
862
|
+
type
|
|
863
|
+
});
|
|
864
|
+
const handleClick = useCallback8(
|
|
865
|
+
(event) => {
|
|
866
|
+
onClick?.(event);
|
|
867
|
+
if (event.defaultPrevented) return;
|
|
868
|
+
handleToggle();
|
|
869
|
+
},
|
|
870
|
+
[handleToggle, onClick]
|
|
871
|
+
);
|
|
872
|
+
return /* @__PURE__ */ jsx13(
|
|
873
|
+
Button,
|
|
874
|
+
{
|
|
875
|
+
ref,
|
|
876
|
+
"aria-label": label,
|
|
877
|
+
"aria-pressed": isActive,
|
|
878
|
+
"data-active-state": isActive ? "on" : "off",
|
|
879
|
+
disabled: !canToggle,
|
|
880
|
+
size: "icon",
|
|
881
|
+
tabIndex: -1,
|
|
882
|
+
type: "button",
|
|
883
|
+
variant: "ghost",
|
|
884
|
+
onClick: handleClick,
|
|
885
|
+
...buttonProps,
|
|
886
|
+
children: /* @__PURE__ */ jsx13(
|
|
887
|
+
Icon,
|
|
888
|
+
{
|
|
889
|
+
className: isActive ? "nt:text-[var(--tt-brand-color-500)]" : void 0
|
|
890
|
+
}
|
|
891
|
+
)
|
|
892
|
+
}
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
);
|
|
896
|
+
MarkButton.displayName = "MarkButton";
|
|
897
|
+
|
|
898
|
+
// src/components/toolbar/toolbar.tsx
|
|
899
|
+
import { forwardRef as forwardRef9 } from "react";
|
|
900
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
901
|
+
var Toolbar = forwardRef9(
|
|
902
|
+
({ children, className, variant = "fixed", ...props }, ref) => {
|
|
903
|
+
const classNames = ["tiptap-toolbar", className].filter(Boolean).join(" ");
|
|
904
|
+
return /* @__PURE__ */ jsx14(
|
|
905
|
+
"div",
|
|
906
|
+
{
|
|
907
|
+
ref,
|
|
908
|
+
"aria-label": "toolbar",
|
|
909
|
+
className: classNames,
|
|
910
|
+
"data-variant": variant,
|
|
911
|
+
role: "toolbar",
|
|
912
|
+
...props,
|
|
913
|
+
children
|
|
914
|
+
}
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
);
|
|
918
|
+
Toolbar.displayName = "Toolbar";
|
|
919
|
+
function ToolbarGroup({
|
|
920
|
+
children,
|
|
921
|
+
className,
|
|
922
|
+
...props
|
|
923
|
+
}) {
|
|
924
|
+
const classNames = ["tiptap-toolbar-group", className].filter(Boolean).join(" ");
|
|
925
|
+
return /* @__PURE__ */ jsx14("div", { className: classNames, role: "group", ...props, children });
|
|
926
|
+
}
|
|
927
|
+
function ToolbarSeparator({
|
|
928
|
+
orientation = "vertical",
|
|
929
|
+
className,
|
|
930
|
+
...props
|
|
931
|
+
}) {
|
|
932
|
+
const classNames = ["tiptap-separator", className].filter(Boolean).join(" ");
|
|
933
|
+
return /* @__PURE__ */ jsx14(
|
|
934
|
+
"div",
|
|
935
|
+
{
|
|
936
|
+
"aria-orientation": orientation === "vertical" ? orientation : void 0,
|
|
937
|
+
className: classNames,
|
|
938
|
+
"data-orientation": orientation,
|
|
939
|
+
role: "separator",
|
|
940
|
+
...props
|
|
941
|
+
}
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/components/ui-primitive/spacer.tsx
|
|
946
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
947
|
+
function Spacer() {
|
|
948
|
+
return /* @__PURE__ */ jsx15("div", { style: { flex: 1 } });
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/components/undo-redo-button/undo-redo-button.tsx
|
|
952
|
+
import { forwardRef as forwardRef10, useCallback as useCallback10 } from "react";
|
|
953
|
+
|
|
954
|
+
// src/components/undo-redo-button/use-undo-redo.ts
|
|
955
|
+
import { Undo2, Redo2 } from "lucide-react";
|
|
956
|
+
import { useCallback as useCallback9, useEffect as useEffect8, useState as useState8 } from "react";
|
|
957
|
+
var actionLabels = {
|
|
958
|
+
undo: "Undo",
|
|
959
|
+
redo: "Redo"
|
|
960
|
+
};
|
|
961
|
+
var actionIcons = {
|
|
962
|
+
undo: Undo2,
|
|
963
|
+
redo: Redo2
|
|
964
|
+
};
|
|
965
|
+
function canExecuteAction(editor, action) {
|
|
966
|
+
if (!editor || !editor.isEditable) return false;
|
|
967
|
+
return action === "undo" ? editor.can().undo() : editor.can().redo();
|
|
968
|
+
}
|
|
969
|
+
function useUndoRedo({ editor, action }) {
|
|
970
|
+
const [canExecute, setCanExecute] = useState8(false);
|
|
971
|
+
useEffect8(() => {
|
|
972
|
+
if (!editor) return;
|
|
973
|
+
const handleUpdate = () => {
|
|
974
|
+
setCanExecute(canExecuteAction(editor, action));
|
|
975
|
+
};
|
|
976
|
+
handleUpdate();
|
|
977
|
+
editor.on("transaction", handleUpdate);
|
|
978
|
+
return () => {
|
|
979
|
+
editor.off("transaction", handleUpdate);
|
|
980
|
+
};
|
|
981
|
+
}, [editor, action]);
|
|
982
|
+
const handleAction = useCallback9(() => {
|
|
983
|
+
if (!editor || !editor.isEditable) return false;
|
|
984
|
+
if (!canExecuteAction(editor, action)) return false;
|
|
985
|
+
const chain = editor.chain().focus();
|
|
986
|
+
return action === "undo" ? chain.undo().run() : chain.redo().run();
|
|
987
|
+
}, [editor, action]);
|
|
988
|
+
return {
|
|
989
|
+
canExecute,
|
|
990
|
+
handleAction,
|
|
991
|
+
label: actionLabels[action],
|
|
992
|
+
Icon: actionIcons[action]
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/components/undo-redo-button/undo-redo-button.tsx
|
|
997
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
998
|
+
var UndoRedoButton = forwardRef10(({ editor, action, onClick, ...buttonProps }, ref) => {
|
|
999
|
+
const { canExecute, handleAction, label, Icon } = useUndoRedo({
|
|
1000
|
+
editor,
|
|
1001
|
+
action
|
|
1002
|
+
});
|
|
1003
|
+
const handleClick = useCallback10(
|
|
1004
|
+
(event) => {
|
|
1005
|
+
onClick?.(event);
|
|
1006
|
+
if (event.defaultPrevented) return;
|
|
1007
|
+
handleAction();
|
|
1008
|
+
},
|
|
1009
|
+
[handleAction, onClick]
|
|
1010
|
+
);
|
|
1011
|
+
return /* @__PURE__ */ jsx16(
|
|
1012
|
+
Button,
|
|
1013
|
+
{
|
|
1014
|
+
ref,
|
|
1015
|
+
"aria-label": label,
|
|
1016
|
+
disabled: !canExecute,
|
|
1017
|
+
size: "icon",
|
|
1018
|
+
tabIndex: -1,
|
|
1019
|
+
type: "button",
|
|
1020
|
+
variant: "ghost",
|
|
1021
|
+
onClick: handleClick,
|
|
1022
|
+
...buttonProps,
|
|
1023
|
+
children: /* @__PURE__ */ jsx16(Icon, {})
|
|
1024
|
+
}
|
|
1025
|
+
);
|
|
1026
|
+
});
|
|
1027
|
+
UndoRedoButton.displayName = "UndoRedoButton";
|
|
1028
|
+
|
|
4
1029
|
// src/hooks/use-markdown-editor.ts
|
|
1030
|
+
import { EditorState } from "@tiptap/pm/state";
|
|
5
1031
|
import { useEditor } from "@tiptap/react";
|
|
6
|
-
import { useEffect, useRef } from "react";
|
|
1032
|
+
import { useEffect as useEffect9, useRef } from "react";
|
|
7
1033
|
|
|
8
1034
|
// src/extensions/shared.ts
|
|
9
1035
|
import { ListKit } from "@tiptap/extension-list";
|
|
@@ -67,15 +1093,28 @@ function useMarkdownEditor({
|
|
|
67
1093
|
);
|
|
68
1094
|
externalValue.current = md;
|
|
69
1095
|
onChangeRef.current(md);
|
|
1096
|
+
},
|
|
1097
|
+
onCreate({ editor: editor2 }) {
|
|
1098
|
+
setTimeout(() => {
|
|
1099
|
+
if (editor2.isDestroyed) return;
|
|
1100
|
+
const { state } = editor2;
|
|
1101
|
+
const freshState = EditorState.create({
|
|
1102
|
+
doc: state.doc,
|
|
1103
|
+
selection: state.selection,
|
|
1104
|
+
plugins: state.plugins
|
|
1105
|
+
});
|
|
1106
|
+
editor2.view.updateState(freshState);
|
|
1107
|
+
editor2.view.dispatch(editor2.view.state.tr);
|
|
1108
|
+
}, 0);
|
|
70
1109
|
}
|
|
71
1110
|
});
|
|
72
|
-
|
|
1111
|
+
useEffect9(() => {
|
|
73
1112
|
if (!editor) return;
|
|
74
1113
|
if (value === externalValue.current) return;
|
|
75
1114
|
externalValue.current = value;
|
|
76
1115
|
editor.commands.setContent(value);
|
|
77
1116
|
}, [value, editor]);
|
|
78
|
-
|
|
1117
|
+
useEffect9(() => {
|
|
79
1118
|
if (!editor) return;
|
|
80
1119
|
editor.setEditable(editable);
|
|
81
1120
|
}, [editable, editor]);
|
|
@@ -83,7 +1122,7 @@ function useMarkdownEditor({
|
|
|
83
1122
|
}
|
|
84
1123
|
|
|
85
1124
|
// src/notra-editor.tsx
|
|
86
|
-
import { jsx } from "react/jsx-runtime";
|
|
1125
|
+
import { jsx as jsx17, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
87
1126
|
function NotraEditor({
|
|
88
1127
|
value,
|
|
89
1128
|
onChange,
|
|
@@ -98,7 +1137,38 @@ function NotraEditor({
|
|
|
98
1137
|
editable: !readOnly
|
|
99
1138
|
});
|
|
100
1139
|
const classNames = ["notra", "notra-editor", className].filter(Boolean).join(" ");
|
|
101
|
-
return /* @__PURE__ */
|
|
1140
|
+
return /* @__PURE__ */ jsxs7("div", { className: classNames, children: [
|
|
1141
|
+
/* @__PURE__ */ jsxs7(Toolbar, { variant: "fixed", children: [
|
|
1142
|
+
/* @__PURE__ */ jsx17(Spacer, {}),
|
|
1143
|
+
/* @__PURE__ */ jsxs7(ToolbarGroup, { children: [
|
|
1144
|
+
/* @__PURE__ */ jsx17(UndoRedoButton, { action: "undo", editor }),
|
|
1145
|
+
/* @__PURE__ */ jsx17(UndoRedoButton, { action: "redo", editor })
|
|
1146
|
+
] }),
|
|
1147
|
+
/* @__PURE__ */ jsx17(ToolbarSeparator, {}),
|
|
1148
|
+
/* @__PURE__ */ jsxs7(ToolbarGroup, { children: [
|
|
1149
|
+
/* @__PURE__ */ jsx17(HeadingDropdownMenu, { editor, levels: [1, 2, 3, 4] }),
|
|
1150
|
+
/* @__PURE__ */ jsx17(
|
|
1151
|
+
ListDropdownMenu,
|
|
1152
|
+
{
|
|
1153
|
+
editor,
|
|
1154
|
+
types: ["bulletList", "orderedList", "taskList"]
|
|
1155
|
+
}
|
|
1156
|
+
),
|
|
1157
|
+
/* @__PURE__ */ jsx17(BlockquoteButton, { editor }),
|
|
1158
|
+
/* @__PURE__ */ jsx17(CodeBlockButton, { editor })
|
|
1159
|
+
] }),
|
|
1160
|
+
/* @__PURE__ */ jsx17(ToolbarSeparator, {}),
|
|
1161
|
+
/* @__PURE__ */ jsxs7(ToolbarGroup, { children: [
|
|
1162
|
+
/* @__PURE__ */ jsx17(MarkButton, { editor, type: "bold" }),
|
|
1163
|
+
/* @__PURE__ */ jsx17(MarkButton, { editor, type: "italic" }),
|
|
1164
|
+
/* @__PURE__ */ jsx17(MarkButton, { editor, type: "strike" }),
|
|
1165
|
+
/* @__PURE__ */ jsx17(MarkButton, { editor, type: "code" }),
|
|
1166
|
+
/* @__PURE__ */ jsx17(LinkPopover, { editor })
|
|
1167
|
+
] }),
|
|
1168
|
+
/* @__PURE__ */ jsx17(Spacer, {})
|
|
1169
|
+
] }),
|
|
1170
|
+
/* @__PURE__ */ jsx17(EditorContent, { className: "notra-editor-content", editor })
|
|
1171
|
+
] });
|
|
102
1172
|
}
|
|
103
1173
|
|
|
104
1174
|
// src/notra-reader.tsx
|
|
@@ -128,7 +1198,7 @@ function markdownToJSON(markdown) {
|
|
|
128
1198
|
}
|
|
129
1199
|
|
|
130
1200
|
// src/notra-reader.tsx
|
|
131
|
-
import { jsx as
|
|
1201
|
+
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
132
1202
|
function NotraReader({ content, className }) {
|
|
133
1203
|
const json = markdownToJSON(content);
|
|
134
1204
|
const rendered = renderToReactElement({
|
|
@@ -136,10 +1206,114 @@ function NotraReader({ content, className }) {
|
|
|
136
1206
|
content: json
|
|
137
1207
|
});
|
|
138
1208
|
const classNames = ["notra", "notra-reader", className].filter(Boolean).join(" ");
|
|
139
|
-
return /* @__PURE__ */
|
|
1209
|
+
return /* @__PURE__ */ jsx18("div", { className: classNames, children: rendered });
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// src/components/ui-primitive/dropdown-menu.tsx
|
|
1213
|
+
import { useEffect as useEffect10, useRef as useRef2, useState as useState9 } from "react";
|
|
1214
|
+
import { createPortal } from "react-dom";
|
|
1215
|
+
import { Fragment, jsx as jsx19, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1216
|
+
function DropdownMenu2({
|
|
1217
|
+
trigger,
|
|
1218
|
+
children,
|
|
1219
|
+
open: controlledOpen,
|
|
1220
|
+
onOpenChange
|
|
1221
|
+
}) {
|
|
1222
|
+
const isControlled = controlledOpen !== void 0;
|
|
1223
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState9(false);
|
|
1224
|
+
const open = isControlled ? controlledOpen : uncontrolledOpen;
|
|
1225
|
+
const triggerRef = useRef2(null);
|
|
1226
|
+
const contentRef = useRef2(null);
|
|
1227
|
+
const [position, setPosition] = useState9({ top: 0, left: 0 });
|
|
1228
|
+
const setOpen = (value) => {
|
|
1229
|
+
if (!isControlled) {
|
|
1230
|
+
setUncontrolledOpen(value);
|
|
1231
|
+
}
|
|
1232
|
+
onOpenChange?.(value);
|
|
1233
|
+
};
|
|
1234
|
+
useEffect10(() => {
|
|
1235
|
+
if (!open || !triggerRef.current) return;
|
|
1236
|
+
const updatePosition = () => {
|
|
1237
|
+
if (!triggerRef.current) return;
|
|
1238
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
1239
|
+
setPosition({
|
|
1240
|
+
top: rect.bottom + 4,
|
|
1241
|
+
left: rect.left + rect.width / 2
|
|
1242
|
+
});
|
|
1243
|
+
};
|
|
1244
|
+
updatePosition();
|
|
1245
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
1246
|
+
window.addEventListener("resize", updatePosition);
|
|
1247
|
+
return () => {
|
|
1248
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
1249
|
+
window.removeEventListener("resize", updatePosition);
|
|
1250
|
+
};
|
|
1251
|
+
}, [open]);
|
|
1252
|
+
useEffect10(() => {
|
|
1253
|
+
if (!open) return;
|
|
1254
|
+
const handleMouseDown = (event) => {
|
|
1255
|
+
const target = event.target;
|
|
1256
|
+
if (triggerRef.current?.contains(target) || contentRef.current?.contains(target)) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
setOpen(false);
|
|
1260
|
+
};
|
|
1261
|
+
document.addEventListener("mousedown", handleMouseDown);
|
|
1262
|
+
return () => document.removeEventListener("mousedown", handleMouseDown);
|
|
1263
|
+
}, [open]);
|
|
1264
|
+
useEffect10(() => {
|
|
1265
|
+
if (!open) return;
|
|
1266
|
+
const handleKeyDown = (event) => {
|
|
1267
|
+
if (event.key === "Escape") {
|
|
1268
|
+
setOpen(false);
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
1272
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
1273
|
+
}, [open]);
|
|
1274
|
+
return /* @__PURE__ */ jsxs8(Fragment, { children: [
|
|
1275
|
+
/* @__PURE__ */ jsx19("div", { ref: triggerRef, onClick: () => setOpen(!open), children: trigger }),
|
|
1276
|
+
open && createPortal(
|
|
1277
|
+
/* @__PURE__ */ jsx19("div", { className: "notra-editor", children: /* @__PURE__ */ jsx19(
|
|
1278
|
+
"div",
|
|
1279
|
+
{
|
|
1280
|
+
ref: contentRef,
|
|
1281
|
+
className: "tiptap-dropdown-menu-content",
|
|
1282
|
+
"data-state": "open",
|
|
1283
|
+
role: "menu",
|
|
1284
|
+
style: {
|
|
1285
|
+
position: "fixed",
|
|
1286
|
+
top: position.top,
|
|
1287
|
+
left: position.left
|
|
1288
|
+
},
|
|
1289
|
+
children: /* @__PURE__ */ jsx19(
|
|
1290
|
+
"div",
|
|
1291
|
+
{
|
|
1292
|
+
className: "tiptap-dropdown-menu-group",
|
|
1293
|
+
onClick: () => setOpen(false),
|
|
1294
|
+
children
|
|
1295
|
+
}
|
|
1296
|
+
)
|
|
1297
|
+
}
|
|
1298
|
+
) }),
|
|
1299
|
+
document.body
|
|
1300
|
+
)
|
|
1301
|
+
] });
|
|
140
1302
|
}
|
|
141
1303
|
export {
|
|
1304
|
+
BlockquoteButton,
|
|
1305
|
+
CodeBlockButton,
|
|
1306
|
+
DropdownMenu2 as DropdownMenu,
|
|
1307
|
+
HeadingDropdownMenu,
|
|
1308
|
+
LinkPopover,
|
|
1309
|
+
ListDropdownMenu,
|
|
1310
|
+
MarkButton,
|
|
142
1311
|
NotraEditor,
|
|
143
|
-
NotraReader
|
|
1312
|
+
NotraReader,
|
|
1313
|
+
Spacer,
|
|
1314
|
+
Toolbar,
|
|
1315
|
+
ToolbarGroup,
|
|
1316
|
+
ToolbarSeparator,
|
|
1317
|
+
UndoRedoButton
|
|
144
1318
|
};
|
|
145
1319
|
//# sourceMappingURL=index.mjs.map
|