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/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
- useEffect(() => {
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
- useEffect(() => {
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__ */ jsx("div", { className: classNames, children: /* @__PURE__ */ jsx(EditorContent, { editor }) });
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 jsx2 } from "react/jsx-runtime";
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__ */ jsx2("div", { className: classNames, children: rendered });
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