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.cjs CHANGED
@@ -30,17 +30,1048 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ BlockquoteButton: () => BlockquoteButton,
34
+ CodeBlockButton: () => CodeBlockButton,
35
+ DropdownMenu: () => DropdownMenu2,
36
+ HeadingDropdownMenu: () => HeadingDropdownMenu,
37
+ LinkPopover: () => LinkPopover,
38
+ ListDropdownMenu: () => ListDropdownMenu,
39
+ MarkButton: () => MarkButton,
33
40
  NotraEditor: () => NotraEditor,
34
- NotraReader: () => NotraReader
41
+ NotraReader: () => NotraReader,
42
+ Spacer: () => Spacer,
43
+ Toolbar: () => Toolbar,
44
+ ToolbarGroup: () => ToolbarGroup,
45
+ ToolbarSeparator: () => ToolbarSeparator,
46
+ UndoRedoButton: () => UndoRedoButton
35
47
  });
36
48
  module.exports = __toCommonJS(index_exports);
49
+ var import_globals = require("./styles/globals.css");
37
50
 
38
51
  // src/notra-editor.tsx
39
- var import_react3 = require("@tiptap/react");
52
+ var import_react18 = require("@tiptap/react");
40
53
 
41
- // src/hooks/use-markdown-editor.ts
42
- var import_react = require("@tiptap/react");
54
+ // src/components/blockquote-button/blockquote-button.tsx
55
+ var import_lucide_react = require("lucide-react");
56
+ var import_react = require("react");
57
+
58
+ // src/components/ui/button.tsx
59
+ var import_class_variance_authority = require("class-variance-authority");
60
+ var import_radix_ui = require("radix-ui");
61
+
62
+ // src/lib/utils.ts
63
+ var import_clsx = require("clsx");
64
+ var import_tailwind_merge = require("tailwind-merge");
65
+ function cn(...inputs) {
66
+ return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
67
+ }
68
+
69
+ // src/components/ui/button.tsx
70
+ var import_jsx_runtime = require("react/jsx-runtime");
71
+ var buttonVariants = (0, import_class_variance_authority.cva)(
72
+ "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",
73
+ {
74
+ variants: {
75
+ variant: {
76
+ default: "nt:bg-primary nt:text-primary-foreground nt:[a]:hover:bg-primary/80",
77
+ 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",
78
+ secondary: "nt:bg-secondary nt:text-secondary-foreground nt:hover:bg-secondary/80 nt:aria-expanded:bg-secondary nt:aria-expanded:text-secondary-foreground",
79
+ 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",
80
+ 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",
81
+ link: "nt:text-primary nt:underline-offset-4 nt:hover:underline"
82
+ },
83
+ size: {
84
+ 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",
85
+ 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",
86
+ 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",
87
+ 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",
88
+ icon: "nt:size-8",
89
+ "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",
90
+ "icon-sm": "nt:size-7 nt:rounded-[min(var(--radius-md),12px)] nt:in-data-[slot=button-group]:rounded-lg",
91
+ "icon-lg": "nt:size-9"
92
+ }
93
+ },
94
+ defaultVariants: {
95
+ variant: "default",
96
+ size: "default"
97
+ }
98
+ }
99
+ );
100
+ function Button({
101
+ className,
102
+ variant = "default",
103
+ size = "default",
104
+ asChild = false,
105
+ ...props
106
+ }) {
107
+ const Comp = asChild ? import_radix_ui.Slot.Root : "button";
108
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
109
+ Comp,
110
+ {
111
+ className: cn(buttonVariants({ variant, size, className })),
112
+ "data-size": size,
113
+ "data-slot": "button",
114
+ "data-variant": variant,
115
+ ...props
116
+ }
117
+ );
118
+ }
119
+
120
+ // src/components/blockquote-button/blockquote-button.tsx
121
+ var import_jsx_runtime2 = require("react/jsx-runtime");
122
+ function canToggleBlockquote(editor) {
123
+ if (!editor || !editor.isEditable) return false;
124
+ return editor.can().toggleWrap("blockquote") || editor.can().clearNodes();
125
+ }
126
+ var BlockquoteButton = (0, import_react.forwardRef)(({ editor, onClick, ...buttonProps }, ref) => {
127
+ const [isActive, setIsActive] = (0, import_react.useState)(false);
128
+ const [canToggle, setCanToggle] = (0, import_react.useState)(false);
129
+ (0, import_react.useEffect)(() => {
130
+ if (!editor) return;
131
+ const update = () => {
132
+ setIsActive(editor.isActive("blockquote"));
133
+ setCanToggle(canToggleBlockquote(editor));
134
+ };
135
+ update();
136
+ editor.on("selectionUpdate", update);
137
+ editor.on("transaction", update);
138
+ return () => {
139
+ editor.off("selectionUpdate", update);
140
+ editor.off("transaction", update);
141
+ };
142
+ }, [editor]);
143
+ const handleClick = (0, import_react.useCallback)(
144
+ (event) => {
145
+ onClick?.(event);
146
+ if (event.defaultPrevented) return;
147
+ if (!editor) return;
148
+ if (editor.isActive("blockquote")) {
149
+ editor.chain().focus().lift("blockquote").run();
150
+ } else {
151
+ editor.chain().focus().clearNodes().wrapIn("blockquote").run();
152
+ }
153
+ },
154
+ [editor, onClick]
155
+ );
156
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
157
+ Button,
158
+ {
159
+ ref,
160
+ "aria-label": "Blockquote",
161
+ "aria-pressed": isActive,
162
+ "data-active-state": isActive ? "on" : "off",
163
+ disabled: !canToggle,
164
+ size: "icon",
165
+ tabIndex: -1,
166
+ type: "button",
167
+ variant: "ghost",
168
+ onClick: handleClick,
169
+ ...buttonProps,
170
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
171
+ import_lucide_react.TextQuote,
172
+ {
173
+ className: isActive ? "nt:text-[var(--tt-brand-color-500)]" : void 0
174
+ }
175
+ )
176
+ }
177
+ );
178
+ });
179
+ BlockquoteButton.displayName = "BlockquoteButton";
180
+
181
+ // src/components/code-block-button/code-block-button.tsx
182
+ var import_lucide_react2 = require("lucide-react");
43
183
  var import_react2 = require("react");
184
+ var import_jsx_runtime3 = require("react/jsx-runtime");
185
+ function canToggleCodeBlock(editor) {
186
+ if (!editor || !editor.isEditable) return false;
187
+ return editor.can().toggleNode("codeBlock", "paragraph") || editor.can().clearNodes();
188
+ }
189
+ var CodeBlockButton = (0, import_react2.forwardRef)(({ editor, onClick, ...buttonProps }, ref) => {
190
+ const [isActive, setIsActive] = (0, import_react2.useState)(false);
191
+ const [canToggle, setCanToggle] = (0, import_react2.useState)(false);
192
+ (0, import_react2.useEffect)(() => {
193
+ if (!editor) return;
194
+ const update = () => {
195
+ setIsActive(editor.isActive("codeBlock"));
196
+ setCanToggle(canToggleCodeBlock(editor));
197
+ };
198
+ update();
199
+ editor.on("selectionUpdate", update);
200
+ editor.on("transaction", update);
201
+ return () => {
202
+ editor.off("selectionUpdate", update);
203
+ editor.off("transaction", update);
204
+ };
205
+ }, [editor]);
206
+ const handleClick = (0, import_react2.useCallback)(
207
+ (event) => {
208
+ onClick?.(event);
209
+ if (event.defaultPrevented) return;
210
+ if (!editor) return;
211
+ if (editor.isActive("codeBlock")) {
212
+ editor.chain().focus().setNode("paragraph").run();
213
+ } else {
214
+ editor.chain().focus().clearNodes().toggleNode("codeBlock", "paragraph").run();
215
+ }
216
+ },
217
+ [editor, onClick]
218
+ );
219
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
220
+ Button,
221
+ {
222
+ ref,
223
+ "aria-label": "Code Block",
224
+ "aria-pressed": isActive,
225
+ "data-active-state": isActive ? "on" : "off",
226
+ disabled: !canToggle,
227
+ size: "icon",
228
+ tabIndex: -1,
229
+ type: "button",
230
+ variant: "ghost",
231
+ onClick: handleClick,
232
+ ...buttonProps,
233
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
234
+ import_lucide_react2.SquareCode,
235
+ {
236
+ className: isActive ? "nt:text-[var(--tt-brand-color-500)]" : void 0
237
+ }
238
+ )
239
+ }
240
+ );
241
+ });
242
+ CodeBlockButton.displayName = "CodeBlockButton";
243
+
244
+ // src/components/heading-dropdown-menu/heading-dropdown-menu.tsx
245
+ var import_lucide_react5 = require("lucide-react");
246
+ var import_react5 = require("react");
247
+
248
+ // src/components/heading-dropdown-menu/heading-menu-item.tsx
249
+ var import_react4 = require("react");
250
+
251
+ // src/components/heading-dropdown-menu/use-heading.ts
252
+ var import_lucide_react3 = require("lucide-react");
253
+ var import_react3 = require("react");
254
+ var headingIcons = {
255
+ 1: import_lucide_react3.Heading1,
256
+ 2: import_lucide_react3.Heading2,
257
+ 3: import_lucide_react3.Heading3,
258
+ 4: import_lucide_react3.Heading4
259
+ };
260
+ var headingLabels = {
261
+ 1: "Heading 1",
262
+ 2: "Heading 2",
263
+ 3: "Heading 3",
264
+ 4: "Heading 4"
265
+ };
266
+ function canToggleHeading(editor, level) {
267
+ if (!editor || !editor.isEditable) return false;
268
+ return editor.can().setNode("heading", { level }) || editor.can().clearNodes();
269
+ }
270
+ function useHeading({
271
+ editor,
272
+ level
273
+ }) {
274
+ const [isActive, setIsActive] = (0, import_react3.useState)(false);
275
+ const [canToggle, setCanToggle] = (0, import_react3.useState)(false);
276
+ (0, import_react3.useEffect)(() => {
277
+ if (!editor) return;
278
+ const handleUpdate = () => {
279
+ setIsActive(editor.isActive("heading", { level }));
280
+ setCanToggle(canToggleHeading(editor, level));
281
+ };
282
+ handleUpdate();
283
+ editor.on("selectionUpdate", handleUpdate);
284
+ editor.on("transaction", handleUpdate);
285
+ return () => {
286
+ editor.off("selectionUpdate", handleUpdate);
287
+ editor.off("transaction", handleUpdate);
288
+ };
289
+ }, [editor, level]);
290
+ const handleToggle = (0, import_react3.useCallback)(() => {
291
+ if (!editor || !editor.isEditable) return false;
292
+ if (editor.isActive("heading", { level })) {
293
+ return editor.chain().focus().setNode("paragraph").run();
294
+ }
295
+ return editor.chain().focus().clearNodes().setNode("heading", { level }).run();
296
+ }, [editor, level]);
297
+ return {
298
+ isActive,
299
+ canToggle,
300
+ handleToggle,
301
+ label: headingLabels[level],
302
+ Icon: headingIcons[level]
303
+ };
304
+ }
305
+ function useActiveHeadingLevel(editor, levels) {
306
+ const [activeLevel, setActiveLevel] = (0, import_react3.useState)(null);
307
+ (0, import_react3.useEffect)(() => {
308
+ if (!editor) return;
309
+ const handleUpdate = () => {
310
+ const found = levels.find(
311
+ (level) => editor.isActive("heading", { level })
312
+ );
313
+ setActiveLevel(found ?? null);
314
+ };
315
+ handleUpdate();
316
+ editor.on("selectionUpdate", handleUpdate);
317
+ editor.on("transaction", handleUpdate);
318
+ return () => {
319
+ editor.off("selectionUpdate", handleUpdate);
320
+ editor.off("transaction", handleUpdate);
321
+ };
322
+ }, [editor, levels]);
323
+ return activeLevel;
324
+ }
325
+ function getHeadingTriggerIcon(activeLevel) {
326
+ if (activeLevel === null) return import_lucide_react3.Heading;
327
+ return headingIcons[activeLevel];
328
+ }
329
+
330
+ // src/components/ui/dropdown-menu.tsx
331
+ var import_lucide_react4 = require("lucide-react");
332
+ var import_radix_ui2 = require("radix-ui");
333
+ var import_jsx_runtime4 = require("react/jsx-runtime");
334
+ function DropdownMenu({
335
+ ...props
336
+ }) {
337
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_radix_ui2.DropdownMenu.Root, { "data-slot": "dropdown-menu", ...props });
338
+ }
339
+ function DropdownMenuTrigger({
340
+ ...props
341
+ }) {
342
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
343
+ import_radix_ui2.DropdownMenu.Trigger,
344
+ {
345
+ "data-slot": "dropdown-menu-trigger",
346
+ ...props
347
+ }
348
+ );
349
+ }
350
+ function DropdownMenuContent({
351
+ className,
352
+ align = "start",
353
+ sideOffset = 4,
354
+ ...props
355
+ }) {
356
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_radix_ui2.DropdownMenu.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
357
+ import_radix_ui2.DropdownMenu.Content,
358
+ {
359
+ align,
360
+ className: cn(
361
+ "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",
362
+ className
363
+ ),
364
+ "data-slot": "dropdown-menu-content",
365
+ sideOffset,
366
+ ...props
367
+ }
368
+ ) });
369
+ }
370
+ function DropdownMenuGroup({
371
+ ...props
372
+ }) {
373
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_radix_ui2.DropdownMenu.Group, { "data-slot": "dropdown-menu-group", ...props });
374
+ }
375
+ function DropdownMenuItem({
376
+ className,
377
+ inset,
378
+ variant = "default",
379
+ ...props
380
+ }) {
381
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
382
+ import_radix_ui2.DropdownMenu.Item,
383
+ {
384
+ className: cn(
385
+ "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",
386
+ className
387
+ ),
388
+ "data-inset": inset,
389
+ "data-slot": "dropdown-menu-item",
390
+ "data-variant": variant,
391
+ ...props
392
+ }
393
+ );
394
+ }
395
+
396
+ // src/components/heading-dropdown-menu/heading-menu-item.tsx
397
+ var import_jsx_runtime5 = require("react/jsx-runtime");
398
+ var HeadingMenuItem = (0, import_react4.forwardRef)(
399
+ ({ editor, level }, ref) => {
400
+ const { isActive, canToggle, handleToggle, label, Icon } = useHeading({
401
+ editor,
402
+ level
403
+ });
404
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
405
+ DropdownMenuItem,
406
+ {
407
+ ref,
408
+ "aria-label": label,
409
+ className: "nt:data-[active-state=on]:bg-accent nt:data-[active-state=on]:text-[var(--tt-brand-color-500)]",
410
+ "data-active-state": isActive ? "on" : "off",
411
+ disabled: !canToggle,
412
+ onSelect: handleToggle,
413
+ children: [
414
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon, {}),
415
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: label })
416
+ ]
417
+ }
418
+ );
419
+ }
420
+ );
421
+ HeadingMenuItem.displayName = "HeadingMenuItem";
422
+
423
+ // src/components/heading-dropdown-menu/heading-dropdown-menu.tsx
424
+ var import_jsx_runtime6 = require("react/jsx-runtime");
425
+ var HeadingDropdownMenu = (0, import_react5.forwardRef)(({ editor, levels = [1, 2, 3, 4], ...buttonProps }, ref) => {
426
+ const activeLevel = useActiveHeadingLevel(editor, levels);
427
+ const TriggerIcon = getHeadingTriggerIcon(activeLevel);
428
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(DropdownMenu, { children: [
429
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
430
+ Button,
431
+ {
432
+ ref,
433
+ "aria-label": "Heading",
434
+ className: "nt:gap-1 nt:px-2",
435
+ "data-active-state": activeLevel !== null ? "on" : "off",
436
+ size: "default",
437
+ tabIndex: -1,
438
+ type: "button",
439
+ variant: "ghost",
440
+ ...buttonProps,
441
+ children: [
442
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
443
+ TriggerIcon,
444
+ {
445
+ className: activeLevel !== null ? "nt:text-[var(--tt-brand-color-500)]" : void 0
446
+ }
447
+ ),
448
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react5.ChevronDown, { className: "nt:size-3" })
449
+ ]
450
+ }
451
+ ) }),
452
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DropdownMenuContent, { align: "start", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DropdownMenuGroup, { children: levels.map((level) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HeadingMenuItem, { editor, level }, level)) }) })
453
+ ] });
454
+ });
455
+ HeadingDropdownMenu.displayName = "HeadingDropdownMenu";
456
+
457
+ // src/components/link-popover/link-popover.tsx
458
+ var import_lucide_react6 = require("lucide-react");
459
+ var import_react7 = require("react");
460
+
461
+ // src/components/link-popover/use-link-popover.ts
462
+ var import_react6 = require("react");
463
+ function useLinkPopover({ editor }) {
464
+ const [url, setUrl] = (0, import_react6.useState)("");
465
+ const [isActive, setIsActive] = (0, import_react6.useState)(false);
466
+ const [canSet, setCanSet] = (0, import_react6.useState)(false);
467
+ (0, import_react6.useEffect)(() => {
468
+ if (!editor) return;
469
+ const handleUpdate = () => {
470
+ const active = editor.isActive("link");
471
+ setIsActive(active);
472
+ setCanSet(editor.isEditable);
473
+ if (active) {
474
+ setUrl(editor.getAttributes("link").href ?? "");
475
+ }
476
+ };
477
+ handleUpdate();
478
+ editor.on("selectionUpdate", handleUpdate);
479
+ editor.on("transaction", handleUpdate);
480
+ return () => {
481
+ editor.off("selectionUpdate", handleUpdate);
482
+ editor.off("transaction", handleUpdate);
483
+ };
484
+ }, [editor]);
485
+ const setLink = (0, import_react6.useCallback)(() => {
486
+ if (!editor) return;
487
+ if (!url) {
488
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
489
+ return;
490
+ }
491
+ editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
492
+ }, [editor, url]);
493
+ const removeLink = (0, import_react6.useCallback)(() => {
494
+ if (!editor) return;
495
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
496
+ setUrl("");
497
+ }, [editor]);
498
+ const openLink = (0, import_react6.useCallback)(() => {
499
+ if (!url) return;
500
+ const sanitized = /^https?:\/\//i.test(url) ? url : `https://${url}`;
501
+ window.open(sanitized, "_blank", "noopener,noreferrer");
502
+ }, [url]);
503
+ return { url, setUrl, isActive, canSet, setLink, removeLink, openLink };
504
+ }
505
+
506
+ // src/components/ui/input.tsx
507
+ var import_jsx_runtime7 = require("react/jsx-runtime");
508
+ function Input({ className, type, ...props }) {
509
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
510
+ "input",
511
+ {
512
+ className: cn(
513
+ "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",
514
+ className
515
+ ),
516
+ "data-slot": "input",
517
+ type,
518
+ ...props
519
+ }
520
+ );
521
+ }
522
+
523
+ // src/components/ui/popover.tsx
524
+ var import_radix_ui3 = require("radix-ui");
525
+ var import_jsx_runtime8 = require("react/jsx-runtime");
526
+ function Popover({
527
+ ...props
528
+ }) {
529
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_radix_ui3.Popover.Root, { "data-slot": "popover", ...props });
530
+ }
531
+ function PopoverTrigger({
532
+ ...props
533
+ }) {
534
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_radix_ui3.Popover.Trigger, { "data-slot": "popover-trigger", ...props });
535
+ }
536
+ function PopoverContent({
537
+ className,
538
+ align = "center",
539
+ sideOffset = 4,
540
+ ...props
541
+ }) {
542
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_radix_ui3.Popover.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
543
+ import_radix_ui3.Popover.Content,
544
+ {
545
+ align,
546
+ className: cn(
547
+ "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",
548
+ className
549
+ ),
550
+ "data-slot": "popover-content",
551
+ sideOffset,
552
+ ...props
553
+ }
554
+ ) });
555
+ }
556
+
557
+ // src/components/ui/separator.tsx
558
+ var import_radix_ui4 = require("radix-ui");
559
+ var import_jsx_runtime9 = require("react/jsx-runtime");
560
+ function Separator({
561
+ className,
562
+ orientation = "horizontal",
563
+ decorative = true,
564
+ ...props
565
+ }) {
566
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
567
+ import_radix_ui4.Separator.Root,
568
+ {
569
+ className: cn(
570
+ "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",
571
+ className
572
+ ),
573
+ "data-slot": "separator",
574
+ decorative,
575
+ orientation,
576
+ ...props
577
+ }
578
+ );
579
+ }
580
+
581
+ // src/components/link-popover/link-popover.tsx
582
+ var import_jsx_runtime10 = require("react/jsx-runtime");
583
+ var LinkPopover = (0, import_react7.forwardRef)(
584
+ ({ editor, ...buttonProps }, ref) => {
585
+ const [isOpen, setIsOpen] = (0, import_react7.useState)(false);
586
+ const { url, setUrl, isActive, canSet, setLink, removeLink, openLink } = useLinkPopover({ editor });
587
+ (0, import_react7.useEffect)(() => {
588
+ if (isActive) {
589
+ setIsOpen(true);
590
+ }
591
+ }, [isActive]);
592
+ const handleSetLink = (0, import_react7.useCallback)(() => {
593
+ setLink();
594
+ setIsOpen(false);
595
+ }, [setLink]);
596
+ const handleRemoveLink = (0, import_react7.useCallback)(() => {
597
+ removeLink();
598
+ setIsOpen(false);
599
+ }, [removeLink]);
600
+ const handleKeyDown = (0, import_react7.useCallback)(
601
+ (event) => {
602
+ if (event.key === "Enter") {
603
+ event.preventDefault();
604
+ handleSetLink();
605
+ }
606
+ },
607
+ [handleSetLink]
608
+ );
609
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Popover, { open: isOpen, onOpenChange: setIsOpen, children: [
610
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
611
+ Button,
612
+ {
613
+ ref,
614
+ "aria-label": "Link",
615
+ "aria-pressed": isActive,
616
+ "data-active-state": isActive ? "on" : "off",
617
+ disabled: !canSet,
618
+ size: "icon",
619
+ tabIndex: -1,
620
+ type: "button",
621
+ variant: "ghost",
622
+ ...buttonProps,
623
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
624
+ import_lucide_react6.Link,
625
+ {
626
+ className: isActive ? "nt:text-[var(--tt-brand-color-500)]" : void 0
627
+ }
628
+ )
629
+ }
630
+ ) }),
631
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
632
+ PopoverContent,
633
+ {
634
+ align: "start",
635
+ className: "nt:flex nt:w-auto nt:items-center nt:gap-1 nt:p-1",
636
+ children: [
637
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
638
+ Input,
639
+ {
640
+ autoFocus: true,
641
+ className: "nt:h-7 nt:min-w-48 nt:border-none nt:shadow-none nt:focus-visible:ring-0",
642
+ placeholder: "Paste a link...",
643
+ type: "url",
644
+ value: url,
645
+ onChange: (e) => setUrl(e.target.value),
646
+ onKeyDown: handleKeyDown
647
+ }
648
+ ),
649
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
650
+ Button,
651
+ {
652
+ "aria-label": "Apply link",
653
+ disabled: !url && !isActive,
654
+ size: "icon-sm",
655
+ tabIndex: -1,
656
+ type: "button",
657
+ variant: "ghost",
658
+ onClick: handleSetLink,
659
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react6.CornerDownLeft, {})
660
+ }
661
+ ),
662
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Separator, { className: "nt:h-5", orientation: "vertical" }),
663
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
664
+ Button,
665
+ {
666
+ "aria-label": "Open link in new window",
667
+ size: "icon-sm",
668
+ tabIndex: -1,
669
+ type: "button",
670
+ variant: "ghost",
671
+ onClick: openLink,
672
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react6.ExternalLink, {})
673
+ }
674
+ ),
675
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
676
+ Button,
677
+ {
678
+ "aria-label": "Remove link",
679
+ size: "icon-sm",
680
+ tabIndex: -1,
681
+ type: "button",
682
+ variant: "ghost",
683
+ onClick: handleRemoveLink,
684
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react6.Trash2, {})
685
+ }
686
+ )
687
+ ]
688
+ }
689
+ )
690
+ ] });
691
+ }
692
+ );
693
+ LinkPopover.displayName = "LinkPopover";
694
+
695
+ // src/components/list-dropdown-menu/list-dropdown-menu.tsx
696
+ var import_lucide_react8 = require("lucide-react");
697
+ var import_react10 = require("react");
698
+
699
+ // src/components/list-dropdown-menu/list-menu-item.tsx
700
+ var import_react9 = require("react");
701
+
702
+ // src/components/list-dropdown-menu/use-list.ts
703
+ var import_lucide_react7 = require("lucide-react");
704
+ var import_react8 = require("react");
705
+ var listIcons = {
706
+ bulletList: import_lucide_react7.List,
707
+ orderedList: import_lucide_react7.ListOrdered,
708
+ taskList: import_lucide_react7.ListTodo
709
+ };
710
+ var listLabels = {
711
+ bulletList: "Bullet List",
712
+ orderedList: "Ordered List",
713
+ taskList: "Task List"
714
+ };
715
+ var listItemTypes = {
716
+ bulletList: "listItem",
717
+ orderedList: "listItem",
718
+ taskList: "taskItem"
719
+ };
720
+ function canToggleList(editor) {
721
+ if (!editor || !editor.isEditable) return false;
722
+ return editor.can().toggleList("bulletList", "listItem") || editor.can().clearNodes();
723
+ }
724
+ function useList({
725
+ editor,
726
+ type
727
+ }) {
728
+ const [isActive, setIsActive] = (0, import_react8.useState)(false);
729
+ const [canToggle, setCanToggle] = (0, import_react8.useState)(false);
730
+ (0, import_react8.useEffect)(() => {
731
+ if (!editor) return;
732
+ const handleUpdate = () => {
733
+ setIsActive(editor.isActive(type));
734
+ setCanToggle(canToggleList(editor));
735
+ };
736
+ handleUpdate();
737
+ editor.on("selectionUpdate", handleUpdate);
738
+ editor.on("transaction", handleUpdate);
739
+ return () => {
740
+ editor.off("selectionUpdate", handleUpdate);
741
+ editor.off("transaction", handleUpdate);
742
+ };
743
+ }, [editor, type]);
744
+ const handleToggle = (0, import_react8.useCallback)(() => {
745
+ if (!editor || !editor.isEditable) return false;
746
+ const itemType = listItemTypes[type];
747
+ if (editor.isActive(type)) {
748
+ return editor.chain().focus().clearNodes().run();
749
+ }
750
+ return editor.chain().focus().clearNodes().toggleList(type, itemType).run();
751
+ }, [editor, type]);
752
+ return {
753
+ isActive,
754
+ canToggle,
755
+ handleToggle,
756
+ label: listLabels[type],
757
+ Icon: listIcons[type]
758
+ };
759
+ }
760
+ function useActiveListType(editor, types) {
761
+ const [activeType, setActiveType] = (0, import_react8.useState)(null);
762
+ (0, import_react8.useEffect)(() => {
763
+ if (!editor) return;
764
+ const handleUpdate = () => {
765
+ const found = types.find((type) => editor.isActive(type));
766
+ setActiveType(found ?? null);
767
+ };
768
+ handleUpdate();
769
+ editor.on("selectionUpdate", handleUpdate);
770
+ editor.on("transaction", handleUpdate);
771
+ return () => {
772
+ editor.off("selectionUpdate", handleUpdate);
773
+ editor.off("transaction", handleUpdate);
774
+ };
775
+ }, [editor, types]);
776
+ return activeType;
777
+ }
778
+ function getListTriggerIcon(activeType) {
779
+ if (activeType === null) return import_lucide_react7.List;
780
+ return listIcons[activeType];
781
+ }
782
+
783
+ // src/components/list-dropdown-menu/list-menu-item.tsx
784
+ var import_jsx_runtime11 = require("react/jsx-runtime");
785
+ var ListMenuItem = (0, import_react9.forwardRef)(
786
+ ({ editor, listType }, ref) => {
787
+ const { isActive, canToggle, handleToggle, label, Icon } = useList({
788
+ editor,
789
+ type: listType
790
+ });
791
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
792
+ DropdownMenuItem,
793
+ {
794
+ ref,
795
+ "aria-label": label,
796
+ className: "nt:data-[active-state=on]:bg-accent nt:data-[active-state=on]:text-[var(--tt-brand-color-500)]",
797
+ "data-active-state": isActive ? "on" : "off",
798
+ disabled: !canToggle,
799
+ onSelect: handleToggle,
800
+ children: [
801
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Icon, {}),
802
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: label })
803
+ ]
804
+ }
805
+ );
806
+ }
807
+ );
808
+ ListMenuItem.displayName = "ListMenuItem";
809
+
810
+ // src/components/list-dropdown-menu/list-dropdown-menu.tsx
811
+ var import_jsx_runtime12 = require("react/jsx-runtime");
812
+ var ListDropdownMenu = (0, import_react10.forwardRef)(
813
+ ({
814
+ editor,
815
+ types = ["bulletList", "orderedList", "taskList"],
816
+ ...buttonProps
817
+ }, ref) => {
818
+ const activeType = useActiveListType(editor, types);
819
+ const TriggerIcon = getListTriggerIcon(activeType);
820
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(DropdownMenu, { children: [
821
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
822
+ Button,
823
+ {
824
+ ref,
825
+ "aria-label": "List",
826
+ className: "nt:gap-1 nt:px-2",
827
+ "data-active-state": activeType !== null ? "on" : "off",
828
+ size: "default",
829
+ tabIndex: -1,
830
+ type: "button",
831
+ variant: "ghost",
832
+ ...buttonProps,
833
+ children: [
834
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
835
+ TriggerIcon,
836
+ {
837
+ className: activeType !== null ? "nt:text-[var(--tt-brand-color-500)]" : void 0
838
+ }
839
+ ),
840
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react8.ChevronDown, { className: "nt:size-3" })
841
+ ]
842
+ }
843
+ ) }),
844
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(DropdownMenuContent, { align: "start", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(DropdownMenuGroup, { children: types.map((type) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ListMenuItem, { editor, listType: type }, type)) }) })
845
+ ] });
846
+ }
847
+ );
848
+ ListDropdownMenu.displayName = "ListDropdownMenu";
849
+
850
+ // src/components/mark-button/mark-button.tsx
851
+ var import_react12 = require("react");
852
+
853
+ // src/components/mark-button/use-mark.ts
854
+ var import_lucide_react9 = require("lucide-react");
855
+ var import_react11 = require("react");
856
+ var markLabels = {
857
+ bold: "Bold",
858
+ italic: "Italic",
859
+ strike: "Strikethrough",
860
+ code: "Code"
861
+ };
862
+ var markIcons = {
863
+ bold: import_lucide_react9.Bold,
864
+ italic: import_lucide_react9.Italic,
865
+ strike: import_lucide_react9.Strikethrough,
866
+ code: import_lucide_react9.Code
867
+ };
868
+ function useMark({ editor, type }) {
869
+ const [isActive, setIsActive] = (0, import_react11.useState)(false);
870
+ const [canToggle, setCanToggle] = (0, import_react11.useState)(false);
871
+ (0, import_react11.useEffect)(() => {
872
+ if (!editor) return;
873
+ const handleUpdate = () => {
874
+ setIsActive(editor.isActive(type));
875
+ setCanToggle(editor.isEditable && editor.can().toggleMark(type));
876
+ };
877
+ handleUpdate();
878
+ editor.on("selectionUpdate", handleUpdate);
879
+ editor.on("transaction", handleUpdate);
880
+ return () => {
881
+ editor.off("selectionUpdate", handleUpdate);
882
+ editor.off("transaction", handleUpdate);
883
+ };
884
+ }, [editor, type]);
885
+ const handleToggle = (0, import_react11.useCallback)(() => {
886
+ if (!editor || !editor.isEditable) return false;
887
+ return editor.chain().focus().toggleMark(type).run();
888
+ }, [editor, type]);
889
+ return {
890
+ isActive,
891
+ canToggle,
892
+ handleToggle,
893
+ label: markLabels[type],
894
+ Icon: markIcons[type]
895
+ };
896
+ }
897
+
898
+ // src/components/mark-button/mark-button.tsx
899
+ var import_jsx_runtime13 = require("react/jsx-runtime");
900
+ var MarkButton = (0, import_react12.forwardRef)(
901
+ ({ editor, type, onClick, ...buttonProps }, ref) => {
902
+ const { isActive, canToggle, handleToggle, label, Icon } = useMark({
903
+ editor,
904
+ type
905
+ });
906
+ const handleClick = (0, import_react12.useCallback)(
907
+ (event) => {
908
+ onClick?.(event);
909
+ if (event.defaultPrevented) return;
910
+ handleToggle();
911
+ },
912
+ [handleToggle, onClick]
913
+ );
914
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
915
+ Button,
916
+ {
917
+ ref,
918
+ "aria-label": label,
919
+ "aria-pressed": isActive,
920
+ "data-active-state": isActive ? "on" : "off",
921
+ disabled: !canToggle,
922
+ size: "icon",
923
+ tabIndex: -1,
924
+ type: "button",
925
+ variant: "ghost",
926
+ onClick: handleClick,
927
+ ...buttonProps,
928
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
929
+ Icon,
930
+ {
931
+ className: isActive ? "nt:text-[var(--tt-brand-color-500)]" : void 0
932
+ }
933
+ )
934
+ }
935
+ );
936
+ }
937
+ );
938
+ MarkButton.displayName = "MarkButton";
939
+
940
+ // src/components/toolbar/toolbar.tsx
941
+ var import_react13 = require("react");
942
+ var import_jsx_runtime14 = require("react/jsx-runtime");
943
+ var Toolbar = (0, import_react13.forwardRef)(
944
+ ({ children, className, variant = "fixed", ...props }, ref) => {
945
+ const classNames = ["tiptap-toolbar", className].filter(Boolean).join(" ");
946
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
947
+ "div",
948
+ {
949
+ ref,
950
+ "aria-label": "toolbar",
951
+ className: classNames,
952
+ "data-variant": variant,
953
+ role: "toolbar",
954
+ ...props,
955
+ children
956
+ }
957
+ );
958
+ }
959
+ );
960
+ Toolbar.displayName = "Toolbar";
961
+ function ToolbarGroup({
962
+ children,
963
+ className,
964
+ ...props
965
+ }) {
966
+ const classNames = ["tiptap-toolbar-group", className].filter(Boolean).join(" ");
967
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: classNames, role: "group", ...props, children });
968
+ }
969
+ function ToolbarSeparator({
970
+ orientation = "vertical",
971
+ className,
972
+ ...props
973
+ }) {
974
+ const classNames = ["tiptap-separator", className].filter(Boolean).join(" ");
975
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
976
+ "div",
977
+ {
978
+ "aria-orientation": orientation === "vertical" ? orientation : void 0,
979
+ className: classNames,
980
+ "data-orientation": orientation,
981
+ role: "separator",
982
+ ...props
983
+ }
984
+ );
985
+ }
986
+
987
+ // src/components/ui-primitive/spacer.tsx
988
+ var import_jsx_runtime15 = require("react/jsx-runtime");
989
+ function Spacer() {
990
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { flex: 1 } });
991
+ }
992
+
993
+ // src/components/undo-redo-button/undo-redo-button.tsx
994
+ var import_react15 = require("react");
995
+
996
+ // src/components/undo-redo-button/use-undo-redo.ts
997
+ var import_lucide_react10 = require("lucide-react");
998
+ var import_react14 = require("react");
999
+ var actionLabels = {
1000
+ undo: "Undo",
1001
+ redo: "Redo"
1002
+ };
1003
+ var actionIcons = {
1004
+ undo: import_lucide_react10.Undo2,
1005
+ redo: import_lucide_react10.Redo2
1006
+ };
1007
+ function canExecuteAction(editor, action) {
1008
+ if (!editor || !editor.isEditable) return false;
1009
+ return action === "undo" ? editor.can().undo() : editor.can().redo();
1010
+ }
1011
+ function useUndoRedo({ editor, action }) {
1012
+ const [canExecute, setCanExecute] = (0, import_react14.useState)(false);
1013
+ (0, import_react14.useEffect)(() => {
1014
+ if (!editor) return;
1015
+ const handleUpdate = () => {
1016
+ setCanExecute(canExecuteAction(editor, action));
1017
+ };
1018
+ handleUpdate();
1019
+ editor.on("transaction", handleUpdate);
1020
+ return () => {
1021
+ editor.off("transaction", handleUpdate);
1022
+ };
1023
+ }, [editor, action]);
1024
+ const handleAction = (0, import_react14.useCallback)(() => {
1025
+ if (!editor || !editor.isEditable) return false;
1026
+ if (!canExecuteAction(editor, action)) return false;
1027
+ const chain = editor.chain().focus();
1028
+ return action === "undo" ? chain.undo().run() : chain.redo().run();
1029
+ }, [editor, action]);
1030
+ return {
1031
+ canExecute,
1032
+ handleAction,
1033
+ label: actionLabels[action],
1034
+ Icon: actionIcons[action]
1035
+ };
1036
+ }
1037
+
1038
+ // src/components/undo-redo-button/undo-redo-button.tsx
1039
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1040
+ var UndoRedoButton = (0, import_react15.forwardRef)(({ editor, action, onClick, ...buttonProps }, ref) => {
1041
+ const { canExecute, handleAction, label, Icon } = useUndoRedo({
1042
+ editor,
1043
+ action
1044
+ });
1045
+ const handleClick = (0, import_react15.useCallback)(
1046
+ (event) => {
1047
+ onClick?.(event);
1048
+ if (event.defaultPrevented) return;
1049
+ handleAction();
1050
+ },
1051
+ [handleAction, onClick]
1052
+ );
1053
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1054
+ Button,
1055
+ {
1056
+ ref,
1057
+ "aria-label": label,
1058
+ disabled: !canExecute,
1059
+ size: "icon",
1060
+ tabIndex: -1,
1061
+ type: "button",
1062
+ variant: "ghost",
1063
+ onClick: handleClick,
1064
+ ...buttonProps,
1065
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Icon, {})
1066
+ }
1067
+ );
1068
+ });
1069
+ UndoRedoButton.displayName = "UndoRedoButton";
1070
+
1071
+ // src/hooks/use-markdown-editor.ts
1072
+ var import_state = require("@tiptap/pm/state");
1073
+ var import_react16 = require("@tiptap/react");
1074
+ var import_react17 = require("react");
44
1075
 
45
1076
  // src/extensions/shared.ts
46
1077
  var import_extension_list = require("@tiptap/extension-list");
@@ -91,10 +1122,10 @@ function useMarkdownEditor({
91
1122
  onChange,
92
1123
  editable = true
93
1124
  }) {
94
- const externalValue = (0, import_react2.useRef)(value);
95
- const onChangeRef = (0, import_react2.useRef)(onChange);
1125
+ const externalValue = (0, import_react17.useRef)(value);
1126
+ const onChangeRef = (0, import_react17.useRef)(onChange);
96
1127
  onChangeRef.current = onChange;
97
- const editor = (0, import_react.useEditor)({
1128
+ const editor = (0, import_react16.useEditor)({
98
1129
  extensions: editorExtensions,
99
1130
  editable,
100
1131
  content: value,
@@ -104,15 +1135,28 @@ function useMarkdownEditor({
104
1135
  );
105
1136
  externalValue.current = md;
106
1137
  onChangeRef.current(md);
1138
+ },
1139
+ onCreate({ editor: editor2 }) {
1140
+ setTimeout(() => {
1141
+ if (editor2.isDestroyed) return;
1142
+ const { state } = editor2;
1143
+ const freshState = import_state.EditorState.create({
1144
+ doc: state.doc,
1145
+ selection: state.selection,
1146
+ plugins: state.plugins
1147
+ });
1148
+ editor2.view.updateState(freshState);
1149
+ editor2.view.dispatch(editor2.view.state.tr);
1150
+ }, 0);
107
1151
  }
108
1152
  });
109
- (0, import_react2.useEffect)(() => {
1153
+ (0, import_react17.useEffect)(() => {
110
1154
  if (!editor) return;
111
1155
  if (value === externalValue.current) return;
112
1156
  externalValue.current = value;
113
1157
  editor.commands.setContent(value);
114
1158
  }, [value, editor]);
115
- (0, import_react2.useEffect)(() => {
1159
+ (0, import_react17.useEffect)(() => {
116
1160
  if (!editor) return;
117
1161
  editor.setEditable(editable);
118
1162
  }, [editable, editor]);
@@ -120,7 +1164,7 @@ function useMarkdownEditor({
120
1164
  }
121
1165
 
122
1166
  // src/notra-editor.tsx
123
- var import_jsx_runtime = require("react/jsx-runtime");
1167
+ var import_jsx_runtime17 = require("react/jsx-runtime");
124
1168
  function NotraEditor({
125
1169
  value,
126
1170
  onChange,
@@ -135,11 +1179,42 @@ function NotraEditor({
135
1179
  editable: !readOnly
136
1180
  });
137
1181
  const classNames = ["notra", "notra-editor", className].filter(Boolean).join(" ");
138
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: classNames, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react3.EditorContent, { editor }) });
1182
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: classNames, children: [
1183
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(Toolbar, { variant: "fixed", children: [
1184
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Spacer, {}),
1185
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(ToolbarGroup, { children: [
1186
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(UndoRedoButton, { action: "undo", editor }),
1187
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(UndoRedoButton, { action: "redo", editor })
1188
+ ] }),
1189
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ToolbarSeparator, {}),
1190
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(ToolbarGroup, { children: [
1191
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(HeadingDropdownMenu, { editor, levels: [1, 2, 3, 4] }),
1192
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1193
+ ListDropdownMenu,
1194
+ {
1195
+ editor,
1196
+ types: ["bulletList", "orderedList", "taskList"]
1197
+ }
1198
+ ),
1199
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(BlockquoteButton, { editor }),
1200
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CodeBlockButton, { editor })
1201
+ ] }),
1202
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ToolbarSeparator, {}),
1203
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(ToolbarGroup, { children: [
1204
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(MarkButton, { editor, type: "bold" }),
1205
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(MarkButton, { editor, type: "italic" }),
1206
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(MarkButton, { editor, type: "strike" }),
1207
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(MarkButton, { editor, type: "code" }),
1208
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(LinkPopover, { editor })
1209
+ ] }),
1210
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Spacer, {})
1211
+ ] }),
1212
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react18.EditorContent, { className: "notra-editor-content", editor })
1213
+ ] });
139
1214
  }
140
1215
 
141
1216
  // src/notra-reader.tsx
142
- var import_react4 = require("@tiptap/static-renderer/pm/react");
1217
+ var import_react19 = require("@tiptap/static-renderer/pm/react");
143
1218
 
144
1219
  // src/utils/markdown-to-json.ts
145
1220
  var import_core = require("@tiptap/core");
@@ -165,19 +1240,123 @@ function markdownToJSON(markdown) {
165
1240
  }
166
1241
 
167
1242
  // src/notra-reader.tsx
168
- var import_jsx_runtime2 = require("react/jsx-runtime");
1243
+ var import_jsx_runtime18 = require("react/jsx-runtime");
169
1244
  function NotraReader({ content, className }) {
170
1245
  const json = markdownToJSON(content);
171
- const rendered = (0, import_react4.renderToReactElement)({
1246
+ const rendered = (0, import_react19.renderToReactElement)({
172
1247
  extensions: sharedExtensions,
173
1248
  content: json
174
1249
  });
175
1250
  const classNames = ["notra", "notra-reader", className].filter(Boolean).join(" ");
176
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: classNames, children: rendered });
1251
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: classNames, children: rendered });
1252
+ }
1253
+
1254
+ // src/components/ui-primitive/dropdown-menu.tsx
1255
+ var import_react20 = require("react");
1256
+ var import_react_dom = require("react-dom");
1257
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1258
+ function DropdownMenu2({
1259
+ trigger,
1260
+ children,
1261
+ open: controlledOpen,
1262
+ onOpenChange
1263
+ }) {
1264
+ const isControlled = controlledOpen !== void 0;
1265
+ const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react20.useState)(false);
1266
+ const open = isControlled ? controlledOpen : uncontrolledOpen;
1267
+ const triggerRef = (0, import_react20.useRef)(null);
1268
+ const contentRef = (0, import_react20.useRef)(null);
1269
+ const [position, setPosition] = (0, import_react20.useState)({ top: 0, left: 0 });
1270
+ const setOpen = (value) => {
1271
+ if (!isControlled) {
1272
+ setUncontrolledOpen(value);
1273
+ }
1274
+ onOpenChange?.(value);
1275
+ };
1276
+ (0, import_react20.useEffect)(() => {
1277
+ if (!open || !triggerRef.current) return;
1278
+ const updatePosition = () => {
1279
+ if (!triggerRef.current) return;
1280
+ const rect = triggerRef.current.getBoundingClientRect();
1281
+ setPosition({
1282
+ top: rect.bottom + 4,
1283
+ left: rect.left + rect.width / 2
1284
+ });
1285
+ };
1286
+ updatePosition();
1287
+ window.addEventListener("scroll", updatePosition, true);
1288
+ window.addEventListener("resize", updatePosition);
1289
+ return () => {
1290
+ window.removeEventListener("scroll", updatePosition, true);
1291
+ window.removeEventListener("resize", updatePosition);
1292
+ };
1293
+ }, [open]);
1294
+ (0, import_react20.useEffect)(() => {
1295
+ if (!open) return;
1296
+ const handleMouseDown = (event) => {
1297
+ const target = event.target;
1298
+ if (triggerRef.current?.contains(target) || contentRef.current?.contains(target)) {
1299
+ return;
1300
+ }
1301
+ setOpen(false);
1302
+ };
1303
+ document.addEventListener("mousedown", handleMouseDown);
1304
+ return () => document.removeEventListener("mousedown", handleMouseDown);
1305
+ }, [open]);
1306
+ (0, import_react20.useEffect)(() => {
1307
+ if (!open) return;
1308
+ const handleKeyDown = (event) => {
1309
+ if (event.key === "Escape") {
1310
+ setOpen(false);
1311
+ }
1312
+ };
1313
+ document.addEventListener("keydown", handleKeyDown);
1314
+ return () => document.removeEventListener("keydown", handleKeyDown);
1315
+ }, [open]);
1316
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
1317
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { ref: triggerRef, onClick: () => setOpen(!open), children: trigger }),
1318
+ open && (0, import_react_dom.createPortal)(
1319
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "notra-editor", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1320
+ "div",
1321
+ {
1322
+ ref: contentRef,
1323
+ className: "tiptap-dropdown-menu-content",
1324
+ "data-state": "open",
1325
+ role: "menu",
1326
+ style: {
1327
+ position: "fixed",
1328
+ top: position.top,
1329
+ left: position.left
1330
+ },
1331
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1332
+ "div",
1333
+ {
1334
+ className: "tiptap-dropdown-menu-group",
1335
+ onClick: () => setOpen(false),
1336
+ children
1337
+ }
1338
+ )
1339
+ }
1340
+ ) }),
1341
+ document.body
1342
+ )
1343
+ ] });
177
1344
  }
178
1345
  // Annotate the CommonJS export names for ESM import in node:
179
1346
  0 && (module.exports = {
1347
+ BlockquoteButton,
1348
+ CodeBlockButton,
1349
+ DropdownMenu,
1350
+ HeadingDropdownMenu,
1351
+ LinkPopover,
1352
+ ListDropdownMenu,
1353
+ MarkButton,
180
1354
  NotraEditor,
181
- NotraReader
1355
+ NotraReader,
1356
+ Spacer,
1357
+ Toolbar,
1358
+ ToolbarGroup,
1359
+ ToolbarSeparator,
1360
+ UndoRedoButton
182
1361
  });
183
1362
  //# sourceMappingURL=index.cjs.map