@kopexa/tiptap 17.11.4 → 17.12.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.
Files changed (3) hide show
  1. package/dist/index.js +1944 -1921
  2. package/dist/index.mjs +1992 -1969
  3. package/package.json +25 -25
package/dist/index.mjs CHANGED
@@ -4435,499 +4435,26 @@ function useUiEditorState(editor) {
4435
4435
  }
4436
4436
 
4437
4437
  // src/ui/bubble-menu/index.tsx
4438
- import { Toolbar, ToolbarGroup, ToolbarSeparator } from "@kopexa/toolbar";
4438
+ import { Toolbar, ToolbarGroup, ToolbarSeparator as ToolbarSeparator2 } from "@kopexa/toolbar";
4439
4439
  import { BubbleMenu as TiptapBubbleMenu } from "@tiptap/react/menus";
4440
4440
 
4441
- // src/ui/link-popover/link-popover.tsx
4442
- import { IconButton as IconButton6 } from "@kopexa/button";
4441
+ // src/ui/color-highlight-button/color-highlight-button.tsx
4443
4442
  import { useTiptapEditor as useTiptapEditor3 } from "@kopexa/editor-utils";
4444
- import {
4445
- CheckIcon,
4446
- EditIcon as EditIcon2,
4447
- ExternalLinkIcon,
4448
- LinkIcon as LinkIcon2,
4449
- TrashIcon as TrashIcon3
4450
- } from "@kopexa/icons";
4451
- import { Input as Input3 } from "@kopexa/input";
4452
- import { Popover } from "@kopexa/popover";
4443
+ import { colorHighlightButton } from "@kopexa/theme";
4453
4444
  import { ToolbarButton } from "@kopexa/toolbar";
4454
- import { useCallback as useCallback15, useEffect as useEffect16, useState as useState15 } from "react";
4445
+ import { useCallback as useCallback15, useMemo as useMemo13 } from "react";
4455
4446
 
4456
- // src/ui/link-popover/use-link-popover.ts
4457
- import { isMarkInSchema, useTiptapEditor as useTiptapEditor2 } from "@kopexa/editor-utils";
4458
- import { LinkIcon } from "@kopexa/icons";
4447
+ // src/ui/color-highlight-button/use-color-highlight.ts
4448
+ import {
4449
+ isMarkInSchema,
4450
+ isNodeTypeSelected,
4451
+ useTiptapEditor as useTiptapEditor2
4452
+ } from "@kopexa/editor-utils";
4453
+ import { HighlighterIcon } from "@kopexa/icons";
4454
+ import { useIsMobile } from "@kopexa/use-is-mobile";
4459
4455
  import * as React7 from "react";
4460
-
4461
- // src/utils/index.ts
4462
- var MAX_FILE_SIZE = 5 * 1024 * 1024;
4463
- var handleImageUpload = async (file, onProgress, abortSignal) => {
4464
- if (!file) {
4465
- throw new Error("No file provided");
4466
- }
4467
- if (file.size > MAX_FILE_SIZE) {
4468
- throw new Error(
4469
- `File size exceeds maximum allowed (${MAX_FILE_SIZE / (1024 * 1024)}MB)`
4470
- );
4471
- }
4472
- for (let progress = 0; progress <= 100; progress += 10) {
4473
- if (abortSignal == null ? void 0 : abortSignal.aborted) {
4474
- throw new Error("Upload cancelled");
4475
- }
4476
- await new Promise((resolve) => setTimeout(resolve, 500));
4477
- onProgress == null ? void 0 : onProgress({ progress });
4478
- }
4479
- return "/images/placeholder-image.png";
4480
- };
4481
- var convertFileToBase64 = (file, abortSignal) => {
4482
- if (!file) {
4483
- return Promise.reject(new Error("No file provided"));
4484
- }
4485
- return new Promise((resolve, reject) => {
4486
- const reader = new FileReader();
4487
- const abortHandler = () => {
4488
- reader.abort();
4489
- reject(new Error("Upload cancelled"));
4490
- };
4491
- if (abortSignal) {
4492
- abortSignal.addEventListener("abort", abortHandler);
4493
- }
4494
- reader.onloadend = () => {
4495
- if (abortSignal) {
4496
- abortSignal.removeEventListener("abort", abortHandler);
4497
- }
4498
- if (typeof reader.result === "string") {
4499
- resolve(reader.result);
4500
- } else {
4501
- reject(new Error("Failed to convert File to base64"));
4502
- }
4503
- };
4504
- reader.onerror = (error) => reject(new Error(`File reading error: ${error}`));
4505
- reader.readAsDataURL(file);
4506
- });
4507
- };
4508
- var ATTR_WHITESPACE = (
4509
- // eslint-disable-next-line no-control-regex
4510
- // biome-ignore lint/suspicious/noControlCharactersInRegex: we can do this yay
4511
- /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g
4512
- );
4513
- function isAllowedUri(uri, protocols) {
4514
- const allowedProtocols = [
4515
- "http",
4516
- "https",
4517
- "ftp",
4518
- "ftps",
4519
- "mailto",
4520
- "tel",
4521
- "callto",
4522
- "sms",
4523
- "cid",
4524
- "xmpp"
4525
- ];
4526
- if (protocols) {
4527
- for (const protocol of protocols) {
4528
- const nextProtocol = typeof protocol === "string" ? protocol : protocol.scheme;
4529
- if (nextProtocol) {
4530
- allowedProtocols.push(nextProtocol);
4531
- }
4532
- }
4533
- }
4534
- return !uri || uri.replace(ATTR_WHITESPACE, "").match(
4535
- new RegExp(
4536
- // eslint-disable-next-line no-useless-escape
4537
- `^(?:(?:${allowedProtocols.join("|")}):|[^a-z]|[a-z0-9+.-]+(?:[^a-z+.-:]|$))`,
4538
- "i"
4539
- )
4540
- );
4541
- }
4542
- function sanitizeUrl(inputUrl, baseUrl, protocols) {
4543
- try {
4544
- const url = new URL(inputUrl, baseUrl);
4545
- if (isAllowedUri(url.href, protocols)) {
4546
- return url.href;
4547
- }
4548
- } catch {
4549
- }
4550
- return "#";
4551
- }
4552
-
4553
- // src/ui/link-popover/use-link-popover.ts
4554
- function canSetLink(editor) {
4555
- if (!editor || !editor.isEditable) return false;
4556
- return editor.can().setMark("link");
4557
- }
4558
- function isLinkActive(editor) {
4559
- if (!editor || !editor.isEditable) return false;
4560
- return editor.isActive("link");
4561
- }
4562
- function shouldShowLinkButton(props) {
4563
- const { editor, hideWhenUnavailable } = props;
4564
- const linkInSchema = isMarkInSchema("link", editor);
4565
- if (!linkInSchema || !editor) {
4566
- return false;
4567
- }
4568
- if (hideWhenUnavailable && !editor.isActive("code")) {
4569
- return canSetLink(editor);
4570
- }
4571
- return true;
4572
- }
4573
- function useLinkHandler(props) {
4574
- const { editor, onSetLink } = props;
4575
- const [url, setUrl] = React7.useState(null);
4576
- React7.useEffect(() => {
4577
- if (!editor) return;
4578
- const { href } = editor.getAttributes("link");
4579
- if (isLinkActive(editor) && url === null) {
4580
- setUrl(href || "");
4581
- }
4582
- }, [editor, url]);
4583
- React7.useEffect(() => {
4584
- if (!editor) return;
4585
- const updateLinkState = () => {
4586
- const { href } = editor.getAttributes("link");
4587
- setUrl(href || "");
4588
- };
4589
- editor.on("selectionUpdate", updateLinkState);
4590
- return () => {
4591
- editor.off("selectionUpdate", updateLinkState);
4592
- };
4593
- }, [editor]);
4594
- const setLink = React7.useCallback(() => {
4595
- if (!url || !editor) return;
4596
- const { selection } = editor.state;
4597
- const isEmpty = selection.empty;
4598
- let chain = editor.chain().focus();
4599
- chain = chain.extendMarkRange("link").setLink({ href: url });
4600
- if (isEmpty) {
4601
- chain = chain.insertContent({ type: "text", text: url });
4602
- }
4603
- chain.run();
4604
- setUrl(null);
4605
- onSetLink == null ? void 0 : onSetLink();
4606
- }, [editor, onSetLink, url]);
4607
- const removeLink = React7.useCallback(() => {
4608
- if (!editor) return;
4609
- editor.chain().focus().extendMarkRange("link").unsetLink().setMeta("preventAutolink", true).run();
4610
- setUrl("");
4611
- }, [editor]);
4612
- const openLink = React7.useCallback(
4613
- (target = "_blank", features = "noopener,noreferrer") => {
4614
- if (!url) return;
4615
- const safeUrl = sanitizeUrl(url, window.location.href);
4616
- if (safeUrl !== "#") {
4617
- window.open(safeUrl, target, features);
4618
- }
4619
- },
4620
- [url]
4621
- );
4622
- return {
4623
- url: url || "",
4624
- setUrl,
4625
- setLink,
4626
- removeLink,
4627
- openLink
4628
- };
4629
- }
4630
- function useLinkState(props) {
4631
- const { editor, hideWhenUnavailable = false } = props;
4632
- const canSet = canSetLink(editor);
4633
- const isActive = isLinkActive(editor);
4634
- const [isVisible, setIsVisible] = React7.useState(false);
4635
- React7.useEffect(() => {
4636
- if (!editor) return;
4637
- const handleSelectionUpdate = () => {
4638
- setIsVisible(
4639
- shouldShowLinkButton({
4640
- editor,
4641
- hideWhenUnavailable
4642
- })
4643
- );
4644
- };
4645
- handleSelectionUpdate();
4646
- editor.on("selectionUpdate", handleSelectionUpdate);
4647
- return () => {
4648
- editor.off("selectionUpdate", handleSelectionUpdate);
4649
- };
4650
- }, [editor, hideWhenUnavailable]);
4651
- return {
4652
- isVisible,
4653
- canSet,
4654
- isActive
4655
- };
4656
- }
4657
- function useLinkPopover(config) {
4658
- const {
4659
- editor: providedEditor,
4660
- hideWhenUnavailable = false,
4661
- onSetLink
4662
- } = config || {};
4663
- const { editor } = useTiptapEditor2(providedEditor);
4664
- const { isVisible, canSet, isActive } = useLinkState({
4665
- editor,
4666
- hideWhenUnavailable
4667
- });
4668
- const linkHandler = useLinkHandler({
4669
- editor,
4670
- onSetLink
4671
- });
4672
- return {
4673
- isVisible,
4674
- canSet,
4675
- isActive,
4676
- label: "Link",
4677
- Icon: LinkIcon,
4678
- ...linkHandler
4679
- };
4680
- }
4681
-
4682
- // src/ui/link-popover/link-popover.tsx
4683
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
4684
- var LinkButton = ({ className, children, ...props }) => {
4685
- return /* @__PURE__ */ jsx16(
4686
- ToolbarButton,
4687
- {
4688
- type: "button",
4689
- className,
4690
- variant: "ghost",
4691
- color: "default",
4692
- tabIndex: -1,
4693
- "aria-label": "Link",
4694
- title: "Link",
4695
- isIconOnly: !children,
4696
- ...props,
4697
- children: children || /* @__PURE__ */ jsx16(LinkIcon2, {})
4698
- }
4699
- );
4700
- };
4701
- var LinkMain = ({
4702
- url,
4703
- setUrl,
4704
- setLink,
4705
- removeLink,
4706
- openLink,
4707
- isActive,
4708
- onSave
4709
- }) => {
4710
- const [isEditing, setIsEditing] = useState15(!isActive || !url);
4711
- useEffect16(() => {
4712
- setIsEditing(!isActive || !url);
4713
- }, [isActive, url]);
4714
- const handleKeyDown = (event) => {
4715
- if (event.key === "Enter") {
4716
- event.preventDefault();
4717
- setLink();
4718
- setIsEditing(false);
4719
- onSave == null ? void 0 : onSave();
4720
- } else if (event.key === "Escape") {
4721
- event.preventDefault();
4722
- setIsEditing(false);
4723
- }
4724
- };
4725
- const handleSave = () => {
4726
- setLink();
4727
- setIsEditing(false);
4728
- onSave == null ? void 0 : onSave();
4729
- };
4730
- const handleEdit = () => {
4731
- setIsEditing(true);
4732
- };
4733
- if (isEditing) {
4734
- return /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1 min-w-[280px]", children: [
4735
- /* @__PURE__ */ jsx16(
4736
- Input3,
4737
- {
4738
- type: "url",
4739
- placeholder: "Enter URL...",
4740
- value: url,
4741
- onChange: (e) => setUrl(e.target.value),
4742
- onKeyDown: handleKeyDown,
4743
- autoComplete: "off",
4744
- autoCorrect: "off",
4745
- autoCapitalize: "off",
4746
- className: "flex-1 h-8 text-sm",
4747
- autoFocus: true
4748
- }
4749
- ),
4750
- /* @__PURE__ */ jsx16(
4751
- IconButton6,
4752
- {
4753
- type: "button",
4754
- size: "sm",
4755
- variant: "ghost",
4756
- onClick: handleSave,
4757
- "aria-label": "Save link",
4758
- disabled: !url,
4759
- children: /* @__PURE__ */ jsx16(CheckIcon, { className: "size-4" })
4760
- }
4761
- )
4762
- ] });
4763
- }
4764
- return /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1 min-w-[280px]", children: [
4765
- /* @__PURE__ */ jsx16(
4766
- "a",
4767
- {
4768
- href: url,
4769
- target: "_blank",
4770
- rel: "noopener noreferrer",
4771
- className: "flex-1 text-sm text-primary truncate max-w-[200px] hover:underline px-2",
4772
- onClick: (e) => {
4773
- e.preventDefault();
4774
- openLink();
4775
- },
4776
- children: url
4777
- }
4778
- ),
4779
- /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-0.5 border-l pl-1 ml-1", children: [
4780
- /* @__PURE__ */ jsx16(
4781
- IconButton6,
4782
- {
4783
- type: "button",
4784
- size: "sm",
4785
- variant: "ghost",
4786
- onClick: openLink,
4787
- "aria-label": "Open link in new tab",
4788
- children: /* @__PURE__ */ jsx16(ExternalLinkIcon, { className: "size-4" })
4789
- }
4790
- ),
4791
- /* @__PURE__ */ jsx16(
4792
- IconButton6,
4793
- {
4794
- type: "button",
4795
- size: "sm",
4796
- variant: "ghost",
4797
- onClick: handleEdit,
4798
- "aria-label": "Edit link",
4799
- children: /* @__PURE__ */ jsx16(EditIcon2, { className: "size-4" })
4800
- }
4801
- ),
4802
- /* @__PURE__ */ jsx16(
4803
- IconButton6,
4804
- {
4805
- type: "button",
4806
- size: "sm",
4807
- variant: "ghost",
4808
- onClick: removeLink,
4809
- "aria-label": "Remove link",
4810
- children: /* @__PURE__ */ jsx16(TrashIcon3, { className: "size-4" })
4811
- }
4812
- )
4813
- ] })
4814
- ] });
4815
- };
4816
- function LinkPopover({
4817
- editor: providedEditor,
4818
- hideWhenUnavailable = false,
4819
- onSetLink,
4820
- onOpenChange,
4821
- autoOpenOnLinkActive = true,
4822
- onClick,
4823
- children,
4824
- ...buttonProps
4825
- }) {
4826
- const { editor } = useTiptapEditor3(providedEditor);
4827
- const [isOpen, setIsOpen] = useState15(false);
4828
- const {
4829
- isVisible,
4830
- canSet,
4831
- isActive,
4832
- url,
4833
- setUrl,
4834
- setLink,
4835
- removeLink,
4836
- openLink,
4837
- label
4838
- } = useLinkPopover({
4839
- editor,
4840
- hideWhenUnavailable,
4841
- onSetLink
4842
- });
4843
- const handleOnOpenChange = useCallback15(
4844
- (nextIsOpen) => {
4845
- setIsOpen(nextIsOpen);
4846
- onOpenChange == null ? void 0 : onOpenChange(nextIsOpen);
4847
- },
4848
- [onOpenChange]
4849
- );
4850
- const handleSetLink = useCallback15(() => {
4851
- setLink();
4852
- setIsOpen(false);
4853
- }, [setLink]);
4854
- const handleClick = useCallback15(
4855
- (event) => {
4856
- onClick == null ? void 0 : onClick(event);
4857
- if (event.defaultPrevented) return;
4858
- setIsOpen(!isOpen);
4859
- },
4860
- [onClick, isOpen]
4861
- );
4862
- useEffect16(() => {
4863
- if (autoOpenOnLinkActive && isActive) {
4864
- setIsOpen(true);
4865
- }
4866
- }, [autoOpenOnLinkActive, isActive]);
4867
- if (!isVisible) {
4868
- return null;
4869
- }
4870
- return /* @__PURE__ */ jsxs12(
4871
- Popover.Root,
4872
- {
4873
- open: isOpen,
4874
- onOpenChange: handleOnOpenChange,
4875
- spacing: "dense",
4876
- children: [
4877
- /* @__PURE__ */ jsx16(
4878
- Popover.Trigger,
4879
- {
4880
- render: /* @__PURE__ */ jsx16(
4881
- LinkButton,
4882
- {
4883
- "data-disabled": !canSet,
4884
- disabled: !canSet,
4885
- "data-active-state": isActive ? "on" : "off",
4886
- "aria-label": label,
4887
- "aria-pressed": isActive,
4888
- onClick: handleClick,
4889
- ...buttonProps
4890
- }
4891
- )
4892
- }
4893
- ),
4894
- /* @__PURE__ */ jsx16(Popover.Content, { children: /* @__PURE__ */ jsx16(
4895
- LinkMain,
4896
- {
4897
- url,
4898
- setUrl,
4899
- setLink: handleSetLink,
4900
- removeLink,
4901
- openLink,
4902
- isActive,
4903
- onSave: () => setIsOpen(false)
4904
- }
4905
- ) })
4906
- ]
4907
- }
4908
- );
4909
- }
4910
- LinkButton.displayName = "LinkButton";
4911
-
4912
- // src/ui/mark-button/index.tsx
4913
- import {
4914
- isMarkInSchema as isMarkInSchema2,
4915
- isNodeTypeSelected,
4916
- useTiptapEditor as useTiptapEditor4
4917
- } from "@kopexa/editor-utils";
4918
- import {
4919
- BoldIcon,
4920
- CodeIcon,
4921
- ItalicIcon,
4922
- StrikeIcon,
4923
- SubscriptIcon,
4924
- SuperscriptIcon,
4925
- UnderlineIcon
4926
- } from "@kopexa/icons";
4927
- import { ToolbarButton as ToolbarButton2 } from "@kopexa/toolbar";
4928
- import { isNodeSelection as isNodeSelection2 } from "@tiptap/react";
4929
- import { useCallback as useCallback16, useMemo as useMemo13 } from "react";
4930
- import { useIntl as useIntl9 } from "react-intl";
4456
+ import { useHotkeys } from "react-hotkeys-hook";
4457
+ import { useIntl as useIntl9 } from "react-intl";
4931
4458
 
4932
4459
  // src/ui/messages.ts
4933
4460
  import { defineMessages as defineMessages7 } from "react-intl";
@@ -5345,545 +4872,593 @@ var messages7 = defineMessages7({
5345
4872
  }
5346
4873
  });
5347
4874
 
5348
- // src/ui/mark-button/index.tsx
5349
- import { jsx as jsx17 } from "react/jsx-runtime";
5350
- var markIcons = {
5351
- bold: BoldIcon,
5352
- italic: ItalicIcon,
5353
- underline: UnderlineIcon,
5354
- strike: StrikeIcon,
5355
- code: CodeIcon,
5356
- superscript: SuperscriptIcon,
5357
- subscript: SubscriptIcon
5358
- };
5359
- var markShortcutKeys = {
5360
- bold: "mod+b",
5361
- italic: "mod+i",
5362
- underline: "mod+u",
5363
- strike: "mod+shift+s",
5364
- code: "mod+e",
5365
- superscript: "mod+.",
5366
- subscript: "mod+,"
5367
- };
5368
- function canToggleMark(editor, type) {
5369
- if (!editor || !editor.isEditable) return false;
5370
- if (!isMarkInSchema2(type, editor) || isNodeTypeSelected(editor, ["image"]))
5371
- return false;
5372
- return editor.can().toggleMark(type);
5373
- }
5374
- function isMarkActive(editor, type) {
5375
- if (!editor) return false;
5376
- return editor.isActive(type);
5377
- }
5378
- function toggleMark(editor, type) {
5379
- if (!editor) return;
5380
- editor.chain().focus().toggleMark(type).run();
5381
- }
5382
- function isMarkButtonDisabled(editor, type, userDisabled = false) {
5383
- if (!editor) return true;
5384
- if (userDisabled) return true;
5385
- if (editor.isActive("codeBlock")) return true;
5386
- if (!canToggleMark(editor, type)) return true;
5387
- return false;
5388
- }
5389
- function shouldShowMarkButton(params) {
5390
- const { editor, type, hideWhenUnavailable, markInSchema } = params;
5391
- if (!markInSchema || !editor) {
5392
- return false;
5393
- }
5394
- if (hideWhenUnavailable) {
5395
- if (isNodeSelection2(editor.state.selection) || !canToggleMark(editor, type)) {
5396
- return false;
5397
- }
5398
- }
5399
- return true;
5400
- }
5401
- var markMessages = {
5402
- bold: messages7.bold,
5403
- italic: messages7.italic,
5404
- underline: messages7.underline,
5405
- strike: messages7.strikethrough,
5406
- code: messages7.code,
5407
- superscript: messages7.superscript,
5408
- subscript: messages7.subscript
5409
- };
5410
- function getTranslatedMarkName(type, intl) {
5411
- return intl.formatMessage(markMessages[type]);
4875
+ // src/ui/color-highlight-button/use-color-highlight.ts
4876
+ var COLOR_HIGHLIGHT_SHORTCUT_KEY = "mod+shift+h";
4877
+ var HIGHLIGHT_COLORS = [
4878
+ {
4879
+ label: "Default background",
4880
+ value: "var(--tt-bg-color)",
4881
+ border: "var(--tt-bg-color-contrast)"
4882
+ },
4883
+ {
4884
+ label: "Gray background",
4885
+ value: "var(--tt-color-highlight-gray)",
4886
+ border: "var(--tt-color-highlight-gray-contrast)"
4887
+ },
4888
+ {
4889
+ label: "Brown background",
4890
+ value: "var(--tt-color-highlight-brown)",
4891
+ border: "var(--tt-color-highlight-brown-contrast)"
4892
+ },
4893
+ {
4894
+ label: "Orange background",
4895
+ value: "var(--tt-color-highlight-orange)",
4896
+ border: "var(--tt-color-highlight-orange-contrast)"
4897
+ },
4898
+ {
4899
+ label: "Yellow background",
4900
+ value: "var(--tt-color-highlight-yellow)",
4901
+ border: "var(--tt-color-highlight-yellow-contrast)"
4902
+ },
4903
+ {
4904
+ label: "Green background",
4905
+ value: "var(--tt-color-highlight-green)",
4906
+ border: "var(--tt-color-highlight-green-contrast)"
4907
+ },
4908
+ {
4909
+ label: "Blue background",
4910
+ value: "var(--tt-color-highlight-blue)",
4911
+ border: "var(--tt-color-highlight-blue-contrast)"
4912
+ },
4913
+ {
4914
+ label: "Purple background",
4915
+ value: "var(--tt-color-highlight-purple)",
4916
+ border: "var(--tt-color-highlight-purple-contrast)"
4917
+ },
4918
+ {
4919
+ label: "Pink background",
4920
+ value: "var(--tt-color-highlight-pink)",
4921
+ border: "var(--tt-color-highlight-pink-contrast)"
4922
+ },
4923
+ {
4924
+ label: "Red background",
4925
+ value: "var(--tt-color-highlight-red)",
4926
+ border: "var(--tt-color-highlight-red-contrast)"
4927
+ }
4928
+ ];
4929
+ function pickHighlightColorsByValue(values) {
4930
+ const colorMap = new Map(
4931
+ HIGHLIGHT_COLORS.map((color) => [color.value, color])
4932
+ );
4933
+ return values.map((value) => colorMap.get(value)).filter((color) => !!color);
5412
4934
  }
5413
- function useMarkState(editor, type, disabled = false) {
4935
+ function canColorHighlight(editor) {
4936
+ if (!editor || !editor.isEditable) return false;
4937
+ if (!isMarkInSchema("highlight", editor) || isNodeTypeSelected(editor, ["image"]))
4938
+ return false;
4939
+ return editor.can().setMark("highlight");
4940
+ }
4941
+ function isColorHighlightActive(editor, highlightColor) {
4942
+ if (!editor || !editor.isEditable) return false;
4943
+ return highlightColor ? editor.isActive("highlight", { color: highlightColor }) : editor.isActive("highlight");
4944
+ }
4945
+ function removeHighlight(editor) {
4946
+ if (!editor || !editor.isEditable) return false;
4947
+ if (!canColorHighlight(editor)) return false;
4948
+ return editor.chain().focus().unsetMark("highlight").run();
4949
+ }
4950
+ function shouldShowButton(props) {
4951
+ const { editor, hideWhenUnavailable } = props;
4952
+ if (!editor || !editor.isEditable) return false;
4953
+ if (!isMarkInSchema("highlight", editor)) return false;
4954
+ if (hideWhenUnavailable && !editor.isActive("code")) {
4955
+ return canColorHighlight(editor);
4956
+ }
4957
+ return true;
4958
+ }
4959
+ function useColorHighlight(config) {
4960
+ const {
4961
+ editor: providedEditor,
4962
+ label,
4963
+ highlightColor,
4964
+ hideWhenUnavailable = false,
4965
+ onApplied
4966
+ } = config;
5414
4967
  const intl = useIntl9();
5415
- const markInSchema = isMarkInSchema2(type, editor);
5416
- const isDisabled = isMarkButtonDisabled(editor, type, disabled);
5417
- const isActive = isMarkActive(editor, type);
5418
- const Icon = markIcons[type];
5419
- const shortcutKey = markShortcutKeys[type];
5420
- const formattedName = getTranslatedMarkName(type, intl);
4968
+ const { editor } = useTiptapEditor2(providedEditor);
4969
+ const isMobile = useIsMobile();
4970
+ const [isVisible, setIsVisible] = React7.useState(true);
4971
+ const canColorHighlightState = canColorHighlight(editor);
4972
+ const isActive = isColorHighlightActive(editor, highlightColor);
4973
+ React7.useEffect(() => {
4974
+ if (!editor) return;
4975
+ const handleSelectionUpdate = () => {
4976
+ setIsVisible(shouldShowButton({ editor, hideWhenUnavailable }));
4977
+ };
4978
+ handleSelectionUpdate();
4979
+ editor.on("selectionUpdate", handleSelectionUpdate);
4980
+ return () => {
4981
+ editor.off("selectionUpdate", handleSelectionUpdate);
4982
+ };
4983
+ }, [editor, hideWhenUnavailable]);
4984
+ const handleColorHighlight = React7.useCallback(() => {
4985
+ if (!editor || !canColorHighlightState || !highlightColor || !label)
4986
+ return false;
4987
+ if (editor.state.storedMarks) {
4988
+ const highlightMarkType = editor.schema.marks.highlight;
4989
+ if (highlightMarkType) {
4990
+ editor.view.dispatch(
4991
+ editor.state.tr.removeStoredMark(highlightMarkType)
4992
+ );
4993
+ }
4994
+ }
4995
+ setTimeout(() => {
4996
+ const success = editor.chain().focus().toggleMark("highlight", { color: highlightColor }).run();
4997
+ if (success) {
4998
+ onApplied == null ? void 0 : onApplied({ color: highlightColor, label });
4999
+ }
5000
+ return success;
5001
+ }, 0);
5002
+ }, [canColorHighlightState, highlightColor, editor, label, onApplied]);
5003
+ const handleRemoveHighlight = React7.useCallback(() => {
5004
+ const success = removeHighlight(editor);
5005
+ if (success) {
5006
+ onApplied == null ? void 0 : onApplied({ color: "", label: "Remove highlight" });
5007
+ }
5008
+ return success;
5009
+ }, [editor, onApplied]);
5010
+ useHotkeys(
5011
+ COLOR_HIGHLIGHT_SHORTCUT_KEY,
5012
+ (event) => {
5013
+ event.preventDefault();
5014
+ handleColorHighlight();
5015
+ },
5016
+ {
5017
+ enabled: isVisible && canColorHighlightState,
5018
+ enableOnContentEditable: !isMobile,
5019
+ enableOnFormTags: true
5020
+ }
5021
+ );
5421
5022
  return {
5422
- markInSchema,
5423
- isDisabled,
5023
+ isVisible,
5424
5024
  isActive,
5425
- Icon,
5426
- shortcutKey,
5427
- formattedName
5025
+ handleColorHighlight,
5026
+ handleRemoveHighlight,
5027
+ canColorHighlight: canColorHighlightState,
5028
+ label: label || intl.formatMessage(messages7.highlight_color),
5029
+ shortcutKeys: COLOR_HIGHLIGHT_SHORTCUT_KEY,
5030
+ Icon: HighlighterIcon
5428
5031
  };
5429
5032
  }
5430
- var MarkButton = ({
5033
+
5034
+ // src/ui/color-highlight-button/color-highlight-button.tsx
5035
+ import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
5036
+ var ColorHighlightButton = ({
5431
5037
  editor: providedEditor,
5432
- type,
5038
+ highlightColor,
5433
5039
  text,
5434
5040
  hideWhenUnavailable = false,
5435
- className = "",
5436
- disabled,
5041
+ onApplied,
5042
+ showShortcut = false,
5437
5043
  onClick,
5438
5044
  children,
5045
+ style,
5046
+ className,
5439
5047
  ...buttonProps
5440
5048
  }) => {
5441
- const { editor } = useTiptapEditor4(providedEditor);
5049
+ const { editor } = useTiptapEditor3(providedEditor);
5442
5050
  const {
5443
- markInSchema,
5444
- isDisabled,
5051
+ isVisible,
5052
+ canColorHighlight: canColorHighlight2,
5445
5053
  isActive,
5446
- Icon,
5447
- shortcutKey,
5448
- formattedName
5449
- } = useMarkState(editor, type, disabled);
5450
- const handleClick = useCallback16(
5451
- (e) => {
5452
- onClick == null ? void 0 : onClick(e);
5453
- if (!e.defaultPrevented && !isDisabled && editor) {
5454
- toggleMark(editor, type);
5455
- }
5054
+ handleColorHighlight,
5055
+ label,
5056
+ shortcutKeys
5057
+ } = useColorHighlight({
5058
+ editor,
5059
+ highlightColor,
5060
+ label: text || `Toggle highlight (${highlightColor})`,
5061
+ hideWhenUnavailable,
5062
+ onApplied
5063
+ });
5064
+ const handleClick = useCallback15(
5065
+ (event) => {
5066
+ onClick == null ? void 0 : onClick(event);
5067
+ if (event.defaultPrevented) return;
5068
+ handleColorHighlight();
5456
5069
  },
5457
- [onClick, isDisabled, editor, type]
5070
+ [handleColorHighlight, onClick]
5458
5071
  );
5459
- const show = useMemo13(() => {
5460
- return shouldShowMarkButton({
5461
- editor,
5462
- type,
5463
- hideWhenUnavailable,
5464
- markInSchema
5465
- });
5466
- }, [editor, type, hideWhenUnavailable, markInSchema]);
5467
- if (!show || !editor || !editor.isEditable) {
5072
+ const buttonStyle = useMemo13(
5073
+ () => ({
5074
+ ...style,
5075
+ "--highlight-color": highlightColor
5076
+ }),
5077
+ [highlightColor, style]
5078
+ );
5079
+ if (!isVisible) {
5468
5080
  return null;
5469
5081
  }
5470
- return /* @__PURE__ */ jsx17(
5471
- ToolbarButton2,
5082
+ const styles = colorHighlightButton();
5083
+ return /* @__PURE__ */ jsxs12(
5084
+ ToolbarButton,
5472
5085
  {
5473
5086
  type: "button",
5474
- className: className.trim(),
5475
- disabled: isDisabled,
5087
+ disabled: !canColorHighlight2,
5088
+ "data-disabled": !canColorHighlight2,
5476
5089
  variant: "ghost",
5477
5090
  color: "default",
5478
5091
  "data-active-state": isActive ? "on" : "off",
5479
- "data-disabled": isDisabled,
5480
5092
  tabIndex: -1,
5481
- "aria-label": formattedName,
5093
+ "aria-label": label,
5094
+ shortcutKeys,
5482
5095
  "aria-pressed": isActive,
5483
- title: formattedName,
5484
- shortcutKeys: shortcutKey,
5485
5096
  onClick: handleClick,
5097
+ style: buttonStyle,
5098
+ className: styles.button({ className }),
5486
5099
  isIconOnly: true,
5487
5100
  ...buttonProps,
5488
- children: /* @__PURE__ */ jsx17(Icon, {})
5101
+ children: [
5102
+ /* @__PURE__ */ jsx16(
5103
+ "span",
5104
+ {
5105
+ "data-active-state": isActive ? "on" : "off",
5106
+ className: styles.mark()
5107
+ }
5108
+ ),
5109
+ children || /* @__PURE__ */ jsxs12(Fragment3, { children: [
5110
+ /* @__PURE__ */ jsx16(
5111
+ "span",
5112
+ {
5113
+ style: { "--highlight-color": highlightColor }
5114
+ }
5115
+ ),
5116
+ text
5117
+ ] })
5118
+ ]
5489
5119
  }
5490
5120
  );
5491
5121
  };
5492
5122
 
5493
- // src/ui/bubble-menu/index.tsx
5494
- import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
5495
- function BubbleMenu({ editor }) {
5496
- if (!editor) {
5497
- return null;
5123
+ // src/ui/color-highlight-popover/color-highlight-popover.tsx
5124
+ import { IconButton as IconButton6 } from "@kopexa/button";
5125
+ import { useTiptapEditor as useTiptapEditor4 } from "@kopexa/editor-utils";
5126
+ import { BanIcon, HighlighterIcon as HighlighterIcon2 } from "@kopexa/icons";
5127
+ import { Popover } from "@kopexa/popover";
5128
+ import { ToolbarSeparator } from "@kopexa/toolbar";
5129
+ import { useMemo as useMemo14, useRef as useRef9, useState as useState15 } from "react";
5130
+ import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
5131
+ var ColorHighlightPopoverButton = ({
5132
+ className,
5133
+ children,
5134
+ ...props
5135
+ }) => /* @__PURE__ */ jsx17(
5136
+ IconButton6,
5137
+ {
5138
+ type: "button",
5139
+ className,
5140
+ variant: "ghost",
5141
+ color: "default",
5142
+ tabIndex: -1,
5143
+ "aria-label": "Highlight text",
5144
+ tooltip: "Highlight",
5145
+ isIconOnly: !children,
5146
+ ...props,
5147
+ children: children != null ? children : /* @__PURE__ */ jsx17(HighlighterIcon2, {})
5498
5148
  }
5499
- return /* @__PURE__ */ jsx18(
5500
- TiptapBubbleMenu,
5501
- {
5502
- editor,
5503
- shouldShow: ({ editor: e, state, view }) => {
5504
- const { selection } = state;
5505
- const { empty } = selection;
5506
- if (!view.hasFocus()) return false;
5507
- if (empty) return false;
5508
- if (e.isActive("codeBlock")) return false;
5509
- if (e.isActive("link")) return false;
5510
- if (e.isActive("variable")) return false;
5511
- if (!e.isEditable) return false;
5512
- return true;
5513
- },
5514
- options: {
5515
- placement: "top",
5516
- offset: 8
5517
- },
5518
- className: "rounded-lg border bg-background shadow-md",
5519
- children: /* @__PURE__ */ jsxs13(Toolbar, { radius: "md", border: "none", className: "p-1", children: [
5520
- /* @__PURE__ */ jsxs13(ToolbarGroup, { children: [
5521
- /* @__PURE__ */ jsx18(MarkButton, { type: "bold" }),
5522
- /* @__PURE__ */ jsx18(MarkButton, { type: "italic" }),
5523
- /* @__PURE__ */ jsx18(MarkButton, { type: "strike" }),
5524
- /* @__PURE__ */ jsx18(MarkButton, { type: "code" })
5525
- ] }),
5526
- /* @__PURE__ */ jsx18(ToolbarSeparator, {}),
5527
- /* @__PURE__ */ jsx18(ToolbarGroup, { children: /* @__PURE__ */ jsx18(LinkPopover, { autoOpenOnLinkActive: false }) })
5528
- ] })
5529
- }
5149
+ );
5150
+ function ColorHighlightPopoverContent({
5151
+ editor,
5152
+ colors = pickHighlightColorsByValue([
5153
+ "var(--tt-color-highlight-green)",
5154
+ "var(--tt-color-highlight-blue)",
5155
+ "var(--tt-color-highlight-red)",
5156
+ "var(--tt-color-highlight-purple)",
5157
+ "var(--tt-color-highlight-yellow)"
5158
+ ])
5159
+ }) {
5160
+ const { handleRemoveHighlight } = useColorHighlight({ editor });
5161
+ const containerRef = useRef9(null);
5162
+ const menuItems = useMemo14(
5163
+ () => [...colors, { label: "Remove highlight", value: "none" }],
5164
+ [colors]
5530
5165
  );
5166
+ const { selectedIndex } = useMenuNavigation({
5167
+ containerRef,
5168
+ items: menuItems,
5169
+ orientation: "both",
5170
+ onSelect: (item) => {
5171
+ if (!containerRef.current) return false;
5172
+ const highlightedElement = containerRef.current.querySelector(
5173
+ '[data-highlighted="true"]'
5174
+ );
5175
+ if (highlightedElement) highlightedElement.click();
5176
+ if (item.value === "none") handleRemoveHighlight();
5177
+ },
5178
+ autoSelectFirstItem: false
5179
+ });
5180
+ return /* @__PURE__ */ jsxs13("div", { ref: containerRef, className: "flex gap-1 items-center", children: [
5181
+ /* @__PURE__ */ jsx17(
5182
+ "div",
5183
+ {
5184
+ className: "flex items-center gap-1 outline-none",
5185
+ "data-orientation": "horizontal",
5186
+ children: colors.map((color, index) => /* @__PURE__ */ jsx17(
5187
+ ColorHighlightButton,
5188
+ {
5189
+ editor,
5190
+ highlightColor: color.value,
5191
+ "aria-label": `${color.label} highlight color`,
5192
+ tabIndex: index === selectedIndex ? 0 : -1,
5193
+ "data-highlighted": selectedIndex === index
5194
+ },
5195
+ color.value
5196
+ ))
5197
+ }
5198
+ ),
5199
+ /* @__PURE__ */ jsx17(ToolbarSeparator, { orientation: "vertical" }),
5200
+ /* @__PURE__ */ jsx17("div", { className: "tiptap-button-group", children: /* @__PURE__ */ jsx17(
5201
+ IconButton6,
5202
+ {
5203
+ onClick: handleRemoveHighlight,
5204
+ "aria-label": "Remove highlight",
5205
+ tabIndex: selectedIndex === colors.length ? 0 : -1,
5206
+ type: "button",
5207
+ role: "menuitem",
5208
+ variant: "ghost",
5209
+ color: "default",
5210
+ "data-highlighted": selectedIndex === colors.length,
5211
+ children: /* @__PURE__ */ jsx17(BanIcon, {})
5212
+ }
5213
+ ) })
5214
+ ] });
5215
+ }
5216
+ function ColorHighlightPopover({
5217
+ editor: providedEditor,
5218
+ colors = pickHighlightColorsByValue([
5219
+ "var(--tt-color-highlight-green)",
5220
+ "var(--tt-color-highlight-blue)",
5221
+ "var(--tt-color-highlight-red)",
5222
+ "var(--tt-color-highlight-purple)",
5223
+ "var(--tt-color-highlight-yellow)"
5224
+ ]),
5225
+ hideWhenUnavailable = false,
5226
+ onApplied,
5227
+ ...props
5228
+ }) {
5229
+ const { editor } = useTiptapEditor4(providedEditor);
5230
+ const [isOpen, setIsOpen] = useState15(false);
5231
+ const { isVisible, canColorHighlight: canColorHighlight2, isActive, label } = useColorHighlight({
5232
+ editor,
5233
+ hideWhenUnavailable,
5234
+ onApplied
5235
+ });
5236
+ if (!isVisible) return null;
5237
+ return /* @__PURE__ */ jsxs13(Popover.Root, { open: isOpen, onOpenChange: setIsOpen, spacing: "dense", children: [
5238
+ /* @__PURE__ */ jsx17(
5239
+ Popover.Trigger,
5240
+ {
5241
+ render: /* @__PURE__ */ jsx17(
5242
+ ColorHighlightPopoverButton,
5243
+ {
5244
+ disabled: !canColorHighlight2,
5245
+ "data-disabled": !canColorHighlight2,
5246
+ "data-active-state": isActive ? "on" : "off",
5247
+ "aria-pressed": isActive,
5248
+ "aria-label": label,
5249
+ title: label,
5250
+ ...props
5251
+ }
5252
+ )
5253
+ }
5254
+ ),
5255
+ /* @__PURE__ */ jsx17(Popover.Content, { "aria-label": "Highlight colors", children: /* @__PURE__ */ jsx17(ColorHighlightPopoverContent, { editor, colors }) })
5256
+ ] });
5531
5257
  }
5532
5258
 
5533
- // src/ui/copy-anchor-link-button/use-scroll-to-hash.ts
5534
- import { getEditorExtension, useTiptapEditor as useTiptapEditor5 } from "@kopexa/editor-utils";
5535
- import * as React9 from "react";
5259
+ // src/ui/link-popover/link-popover.tsx
5260
+ import { IconButton as IconButton7 } from "@kopexa/button";
5261
+ import { useTiptapEditor as useTiptapEditor6 } from "@kopexa/editor-utils";
5262
+ import {
5263
+ CheckIcon,
5264
+ EditIcon as EditIcon2,
5265
+ ExternalLinkIcon,
5266
+ LinkIcon as LinkIcon2,
5267
+ TrashIcon as TrashIcon3
5268
+ } from "@kopexa/icons";
5269
+ import { Input as Input3 } from "@kopexa/input";
5270
+ import { Popover as Popover2 } from "@kopexa/popover";
5271
+ import { ToolbarButton as ToolbarButton2 } from "@kopexa/toolbar";
5272
+ import { useCallback as useCallback17, useEffect as useEffect17, useState as useState17 } from "react";
5536
5273
 
5537
- // src/hooks/use-floating-toolbar-visibility.ts
5538
- import { NodeSelection } from "@tiptap/pm/state";
5539
- import { isNodeSelection as isNodeSelection3 } from "@tiptap/react";
5274
+ // src/ui/link-popover/use-link-popover.ts
5275
+ import { isMarkInSchema as isMarkInSchema2, useTiptapEditor as useTiptapEditor5 } from "@kopexa/editor-utils";
5276
+ import { LinkIcon } from "@kopexa/icons";
5540
5277
  import * as React8 from "react";
5541
- var HIDE_FLOATING_META = "hideFloatingToolbar";
5542
- var selectNodeAndHideFloating = (editor, pos) => {
5543
- if (!editor) return;
5544
- const { state, view } = editor;
5545
- view.dispatch(
5546
- state.tr.setSelection(NodeSelection.create(state.doc, pos)).setMeta(HIDE_FLOATING_META, true)
5547
- );
5548
- };
5549
5278
 
5550
- // src/ui/copy-anchor-link-button/use-scroll-to-hash.ts
5551
- function useScrollToHash(config = {}) {
5552
- const {
5553
- editor: providedEditor,
5554
- onTargetFound = () => {
5555
- },
5556
- onTargetNotFound = () => {
5557
- }
5558
- } = config;
5559
- const { editor } = useTiptapEditor5(providedEditor);
5560
- const scrollToNode = React9.useCallback(
5561
- (id) => {
5562
- var _a, _b, _c;
5563
- if (!editor) return false;
5564
- const attributeName = (_c = (_b = (_a = getEditorExtension(editor, "uniqueID")) == null ? void 0 : _a.options) == null ? void 0 : _b.attributeName) != null ? _c : "data-id";
5565
- let position = null;
5566
- editor.state.doc.descendants((node, pos) => {
5567
- var _a2;
5568
- if (((_a2 = node.attrs) == null ? void 0 : _a2[attributeName]) === id) {
5569
- position = pos;
5570
- return false;
5571
- }
5572
- return true;
5573
- });
5574
- if (position === null) return false;
5575
- selectNodeAndHideFloating(editor, position);
5576
- setTimeout(() => {
5577
- let dom = null;
5578
- if (typeof position === "number") {
5579
- dom = editor.view.nodeDOM(position);
5580
- }
5581
- if (dom) {
5582
- dom.scrollIntoView({ behavior: "smooth", block: "center" });
5583
- }
5584
- }, 0);
5585
- return true;
5586
- },
5587
- [editor]
5588
- );
5589
- const handleScroll = React9.useCallback(
5590
- (delay = 0) => {
5591
- var _a;
5592
- const hash = (_a = window.location.hash) == null ? void 0 : _a.substring(1);
5593
- if (!hash) return;
5594
- setTimeout(() => {
5595
- if (scrollToNode(hash)) {
5596
- onTargetFound(hash);
5597
- } else {
5598
- onTargetNotFound(hash);
5599
- }
5600
- }, delay);
5601
- },
5602
- [scrollToNode, onTargetFound, onTargetNotFound]
5603
- );
5604
- React9.useEffect(() => {
5605
- var _a, _b;
5606
- if (!editor) return;
5607
- const provider = (_b = (_a = editor.extensionManager.extensions.find(
5608
- (ext) => ext.name === "collaborationCaret"
5609
- )) == null ? void 0 : _a.options) == null ? void 0 : _b.provider;
5610
- if (provider == null ? void 0 : provider.on) {
5611
- const syncHandler = () => handleScroll(500);
5612
- provider.on("synced", syncHandler);
5613
- return () => {
5614
- var _a2;
5615
- (_a2 = provider.off) == null ? void 0 : _a2.call(provider, "synced", syncHandler);
5616
- };
5617
- } else {
5618
- handleScroll(500);
5279
+ // src/utils/index.ts
5280
+ var MAX_FILE_SIZE = 5 * 1024 * 1024;
5281
+ var handleImageUpload = async (file, onProgress, abortSignal) => {
5282
+ if (!file) {
5283
+ throw new Error("No file provided");
5284
+ }
5285
+ if (file.size > MAX_FILE_SIZE) {
5286
+ throw new Error(
5287
+ `File size exceeds maximum allowed (${MAX_FILE_SIZE / (1024 * 1024)}MB)`
5288
+ );
5289
+ }
5290
+ for (let progress = 0; progress <= 100; progress += 10) {
5291
+ if (abortSignal == null ? void 0 : abortSignal.aborted) {
5292
+ throw new Error("Upload cancelled");
5619
5293
  }
5620
- }, [editor, handleScroll]);
5621
- React9.useEffect(() => {
5622
- const immediateScroll = () => handleScroll();
5623
- const delayedScroll = () => handleScroll(500);
5624
- window.addEventListener("hashchange", immediateScroll);
5625
- window.addEventListener("pageshow", delayedScroll);
5626
- window.addEventListener("popstate", immediateScroll);
5627
- return () => {
5628
- window.removeEventListener("hashchange", immediateScroll);
5629
- window.removeEventListener("pageshow", delayedScroll);
5630
- window.removeEventListener("popstate", immediateScroll);
5631
- };
5632
- }, [handleScroll]);
5633
- return { scrollToHash: scrollToNode };
5634
- }
5635
-
5636
- // src/ui/link-bubble/index.tsx
5637
- import { IconButton as IconButton7 } from "@kopexa/button";
5638
- import { EditIcon as EditIcon3, ExternalLinkIcon as ExternalLinkIcon2, TrashIcon as TrashIcon4 } from "@kopexa/icons";
5639
- import { Input as Input4 } from "@kopexa/input";
5640
- import { BubbleMenu as TiptapBubbleMenu2 } from "@tiptap/react/menus";
5641
- import { useCallback as useCallback18, useEffect as useEffect19, useState as useState17 } from "react";
5642
- import { useIntl as useIntl10 } from "react-intl";
5643
- import { Fragment as Fragment3, jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
5644
- function LinkBubble({ editor }) {
5645
- const intl = useIntl10();
5646
- const [isEditing, setIsEditing] = useState17(false);
5647
- const [url, setUrl] = useState17("");
5648
- const getCurrentUrl = useCallback18(() => {
5649
- if (!editor) return "";
5650
- const attrs = editor.getAttributes("link");
5651
- return attrs.href || "";
5652
- }, [editor]);
5653
- useEffect19(() => {
5654
- const isLinkActive2 = editor == null ? void 0 : editor.isActive("link");
5655
- if (isLinkActive2) {
5656
- setUrl(getCurrentUrl());
5657
- setIsEditing(false);
5658
- }
5659
- }, [editor, getCurrentUrl]);
5660
- const handleOpenLink = useCallback18(() => {
5661
- const href = getCurrentUrl();
5662
- if (href) {
5663
- window.open(href, "_blank", "noopener,noreferrer");
5664
- }
5665
- }, [getCurrentUrl]);
5666
- const handleRemoveLink = useCallback18(() => {
5667
- editor == null ? void 0 : editor.chain().focus().unsetLink().run();
5668
- }, [editor]);
5669
- const handleEdit = useCallback18(() => {
5670
- setUrl(getCurrentUrl());
5671
- setIsEditing(true);
5672
- }, [getCurrentUrl]);
5673
- const handleSave = useCallback18(() => {
5674
- if (url) {
5675
- editor == null ? void 0 : editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
5676
- } else {
5677
- editor == null ? void 0 : editor.chain().focus().unsetLink().run();
5294
+ await new Promise((resolve) => setTimeout(resolve, 500));
5295
+ onProgress == null ? void 0 : onProgress({ progress });
5296
+ }
5297
+ return "/images/placeholder-image.png";
5298
+ };
5299
+ var convertFileToBase64 = (file, abortSignal) => {
5300
+ if (!file) {
5301
+ return Promise.reject(new Error("No file provided"));
5302
+ }
5303
+ return new Promise((resolve, reject) => {
5304
+ const reader = new FileReader();
5305
+ const abortHandler = () => {
5306
+ reader.abort();
5307
+ reject(new Error("Upload cancelled"));
5308
+ };
5309
+ if (abortSignal) {
5310
+ abortSignal.addEventListener("abort", abortHandler);
5678
5311
  }
5679
- setIsEditing(false);
5680
- }, [editor, url]);
5681
- const handleKeyDown = useCallback18(
5682
- (e) => {
5683
- if (e.key === "Enter") {
5684
- e.preventDefault();
5685
- handleSave();
5686
- } else if (e.key === "Escape") {
5687
- e.preventDefault();
5688
- setIsEditing(false);
5689
- setUrl(getCurrentUrl());
5312
+ reader.onloadend = () => {
5313
+ if (abortSignal) {
5314
+ abortSignal.removeEventListener("abort", abortHandler);
5315
+ }
5316
+ if (typeof reader.result === "string") {
5317
+ resolve(reader.result);
5318
+ } else {
5319
+ reject(new Error("Failed to convert File to base64"));
5320
+ }
5321
+ };
5322
+ reader.onerror = (error) => reject(new Error(`File reading error: ${error}`));
5323
+ reader.readAsDataURL(file);
5324
+ });
5325
+ };
5326
+ var ATTR_WHITESPACE = (
5327
+ // eslint-disable-next-line no-control-regex
5328
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: we can do this yay
5329
+ /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g
5330
+ );
5331
+ function isAllowedUri(uri, protocols) {
5332
+ const allowedProtocols = [
5333
+ "http",
5334
+ "https",
5335
+ "ftp",
5336
+ "ftps",
5337
+ "mailto",
5338
+ "tel",
5339
+ "callto",
5340
+ "sms",
5341
+ "cid",
5342
+ "xmpp"
5343
+ ];
5344
+ if (protocols) {
5345
+ for (const protocol of protocols) {
5346
+ const nextProtocol = typeof protocol === "string" ? protocol : protocol.scheme;
5347
+ if (nextProtocol) {
5348
+ allowedProtocols.push(nextProtocol);
5690
5349
  }
5691
- },
5692
- [handleSave, getCurrentUrl]
5693
- );
5694
- if (!editor) {
5695
- return null;
5696
- }
5697
- return /* @__PURE__ */ jsx19(
5698
- TiptapBubbleMenu2,
5699
- {
5700
- editor,
5701
- pluginKey: "linkBubbleMenu",
5702
- shouldShow: ({ editor: e, view }) => {
5703
- if (!view.hasFocus()) return false;
5704
- return e.isActive("link") && e.isEditable;
5705
- },
5706
- options: {
5707
- placement: "bottom-start",
5708
- offset: 8
5709
- },
5710
- className: "rounded-lg border bg-background shadow-md",
5711
- children: /* @__PURE__ */ jsx19("div", { className: "flex items-center gap-1 p-1.5 min-w-[280px]", children: isEditing ? /* @__PURE__ */ jsxs14(Fragment3, { children: [
5712
- /* @__PURE__ */ jsx19(
5713
- Input4,
5714
- {
5715
- type: "url",
5716
- value: url,
5717
- onChange: (e) => setUrl(e.target.value),
5718
- onKeyDown: handleKeyDown,
5719
- placeholder: intl.formatMessage(messages7.link_placeholder),
5720
- className: "flex-1 h-8 text-sm",
5721
- autoFocus: true
5722
- }
5723
- ),
5724
- /* @__PURE__ */ jsx19(
5725
- IconButton7,
5726
- {
5727
- type: "button",
5728
- size: "sm",
5729
- variant: "ghost",
5730
- onClick: handleSave,
5731
- "aria-label": intl.formatMessage(messages7.link_save),
5732
- children: /* @__PURE__ */ jsx19(EditIcon3, { className: "size-4" })
5733
- }
5734
- )
5735
- ] }) : /* @__PURE__ */ jsxs14(Fragment3, { children: [
5736
- /* @__PURE__ */ jsx19(
5737
- "a",
5738
- {
5739
- href: getCurrentUrl(),
5740
- target: "_blank",
5741
- rel: "noopener noreferrer",
5742
- className: "flex-1 text-sm text-primary truncate max-w-[200px] hover:underline px-2",
5743
- onClick: (e) => {
5744
- e.preventDefault();
5745
- handleOpenLink();
5746
- },
5747
- children: getCurrentUrl()
5748
- }
5749
- ),
5750
- /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-0.5 border-l pl-1 ml-1", children: [
5751
- /* @__PURE__ */ jsx19(
5752
- IconButton7,
5753
- {
5754
- type: "button",
5755
- size: "sm",
5756
- variant: "ghost",
5757
- onClick: handleOpenLink,
5758
- "aria-label": intl.formatMessage(messages7.link_open),
5759
- children: /* @__PURE__ */ jsx19(ExternalLinkIcon2, { className: "size-4" })
5760
- }
5761
- ),
5762
- /* @__PURE__ */ jsx19(
5763
- IconButton7,
5764
- {
5765
- type: "button",
5766
- size: "sm",
5767
- variant: "ghost",
5768
- onClick: handleEdit,
5769
- "aria-label": intl.formatMessage(messages7.link_edit),
5770
- children: /* @__PURE__ */ jsx19(EditIcon3, { className: "size-4" })
5771
- }
5772
- ),
5773
- /* @__PURE__ */ jsx19(
5774
- IconButton7,
5775
- {
5776
- type: "button",
5777
- size: "sm",
5778
- variant: "ghost",
5779
- onClick: handleRemoveLink,
5780
- "aria-label": intl.formatMessage(messages7.link_remove),
5781
- children: /* @__PURE__ */ jsx19(TrashIcon4, { className: "size-4" })
5782
- }
5783
- )
5784
- ] })
5785
- ] }) })
5786
5350
  }
5351
+ }
5352
+ return !uri || uri.replace(ATTR_WHITESPACE, "").match(
5353
+ new RegExp(
5354
+ // eslint-disable-next-line no-useless-escape
5355
+ `^(?:(?:${allowedProtocols.join("|")}):|[^a-z]|[a-z0-9+.-]+(?:[^a-z+.-:]|$))`,
5356
+ "i"
5357
+ )
5787
5358
  );
5788
5359
  }
5789
-
5790
- // src/ui/slash-dropdown-menu/slash-dropdown-menu.tsx
5791
- import { Button as Button6 } from "@kopexa/button";
5792
- import { getElementOverflowPosition as getElementOverflowPosition2 } from "@kopexa/editor-utils";
5793
- import { Separator } from "@kopexa/separator";
5794
- import { slashDropdownMenu as slashDropdownMenu2 } from "@kopexa/theme";
5795
- import * as React11 from "react";
5796
-
5797
- // src/ui/slash-dropdown-menu/use-slash-dropdown-menu.ts
5798
- import {
5799
- findSelectionPosition,
5800
- hasContentAbove,
5801
- isExtensionAvailable,
5802
- isNodeInSchema as isNodeInSchema2
5803
- } from "@kopexa/editor-utils";
5804
- import {
5805
- AiSparklesIcon,
5806
- AlertIcon as AlertIcon2,
5807
- BlockquoteIcon,
5808
- CodeBlockIcon,
5809
- ControlsIcon,
5810
- HeadingOneIcon,
5811
- HeadingThreeIcon,
5812
- HeadingTwoIcon,
5813
- ImageIcon as ImageIcon2,
5814
- InfoIcon as InfoIcon2,
5815
- ListIcon as ListIcon2,
5816
- ListOrderedIcon,
5817
- ListTodoIcon,
5818
- MinusIcon,
5819
- TableIcon as TableIcon2,
5820
- TableOfContentsIcon,
5821
- TypeIcon
5822
- } from "@kopexa/icons";
5823
- import * as React10 from "react";
5824
- import { useIntl as useIntl12 } from "react-intl";
5825
-
5826
- // src/ui/table-button/use-table.ts
5827
- import {
5828
- isNodeInSchema,
5829
- isNodeTypeSelected as isNodeTypeSelected2,
5830
- useTiptapEditor as useTiptapEditor6
5831
- } from "@kopexa/editor-utils";
5832
- import { TableIcon } from "@kopexa/icons";
5833
- import { isNodeSelection as isNodeSelection4 } from "@tiptap/react";
5834
- import { useCallback as useCallback19, useEffect as useEffect20, useState as useState18 } from "react";
5835
- import { useIntl as useIntl11 } from "react-intl";
5836
- function canToggle(editor) {
5837
- if (!editor || !editor.isEditable) return false;
5838
- if (!isNodeInSchema("table", editor) || isNodeTypeSelected2(editor, ["image"])) {
5839
- return false;
5840
- }
5360
+ function sanitizeUrl(inputUrl, baseUrl, protocols) {
5841
5361
  try {
5842
- return editor.can().insertTable({ rows: 3, cols: 3, withHeaderRow: true });
5362
+ const url = new URL(inputUrl, baseUrl);
5363
+ if (isAllowedUri(url.href, protocols)) {
5364
+ return url.href;
5365
+ }
5843
5366
  } catch {
5844
- return false;
5845
5367
  }
5368
+ return "#";
5846
5369
  }
5847
- function toggleTable(editor, config) {
5848
- var _a;
5370
+
5371
+ // src/ui/link-popover/use-link-popover.ts
5372
+ function canSetLink(editor) {
5849
5373
  if (!editor || !editor.isEditable) return false;
5850
- if (!canToggle(editor)) return false;
5851
- try {
5852
- return editor.chain().focus().insertTable({
5853
- rows: (config == null ? void 0 : config.rows) || 3,
5854
- cols: (config == null ? void 0 : config.cols) || 3,
5855
- withHeaderRow: (_a = config == null ? void 0 : config.withHeaderRow) != null ? _a : true
5856
- }).run();
5857
- } catch {
5858
- return false;
5859
- }
5374
+ return editor.can().setMark("link");
5860
5375
  }
5861
- function shouldShowButton(props) {
5862
- const { editor, hideWhenUnavailable } = props;
5376
+ function isLinkActive(editor) {
5863
5377
  if (!editor || !editor.isEditable) return false;
5864
- if (!isNodeInSchema("table", editor)) return false;
5865
- if (hideWhenUnavailable) {
5866
- if (isNodeSelection4(editor.state.selection) || !canToggle) {
5867
- return false;
5868
- }
5378
+ return editor.isActive("link");
5379
+ }
5380
+ function shouldShowLinkButton(props) {
5381
+ const { editor, hideWhenUnavailable } = props;
5382
+ const linkInSchema = isMarkInSchema2("link", editor);
5383
+ if (!linkInSchema || !editor) {
5384
+ return false;
5385
+ }
5386
+ if (hideWhenUnavailable && !editor.isActive("code")) {
5387
+ return canSetLink(editor);
5869
5388
  }
5870
5389
  return true;
5871
5390
  }
5872
- function useTableBlock(config) {
5873
- const {
5874
- editor: providedEditor,
5875
- hideWhenUnavailable = false,
5876
- onToggled
5877
- } = config || {};
5878
- const intl = useIntl11();
5879
- const { editor } = useTiptapEditor6(providedEditor);
5880
- const [isVisible, setIsVisible] = useState18(true);
5881
- const canToggleState = canToggle(editor);
5882
- const isActive = (editor == null ? void 0 : editor.isActive("table")) || false;
5883
- useEffect20(() => {
5391
+ function useLinkHandler(props) {
5392
+ const { editor, onSetLink } = props;
5393
+ const [url, setUrl] = React8.useState(null);
5394
+ React8.useEffect(() => {
5395
+ if (!editor) return;
5396
+ const { href } = editor.getAttributes("link");
5397
+ if (isLinkActive(editor) && url === null) {
5398
+ setUrl(href || "");
5399
+ }
5400
+ }, [editor, url]);
5401
+ React8.useEffect(() => {
5402
+ if (!editor) return;
5403
+ const updateLinkState = () => {
5404
+ const { href } = editor.getAttributes("link");
5405
+ setUrl(href || "");
5406
+ };
5407
+ editor.on("selectionUpdate", updateLinkState);
5408
+ return () => {
5409
+ editor.off("selectionUpdate", updateLinkState);
5410
+ };
5411
+ }, [editor]);
5412
+ const setLink = React8.useCallback(() => {
5413
+ if (!url || !editor) return;
5414
+ const { selection } = editor.state;
5415
+ const isEmpty = selection.empty;
5416
+ let chain = editor.chain().focus();
5417
+ chain = chain.extendMarkRange("link").setLink({ href: url });
5418
+ if (isEmpty) {
5419
+ chain = chain.insertContent({ type: "text", text: url });
5420
+ }
5421
+ chain.run();
5422
+ setUrl(null);
5423
+ onSetLink == null ? void 0 : onSetLink();
5424
+ }, [editor, onSetLink, url]);
5425
+ const removeLink = React8.useCallback(() => {
5426
+ if (!editor) return;
5427
+ editor.chain().focus().extendMarkRange("link").unsetLink().setMeta("preventAutolink", true).run();
5428
+ setUrl("");
5429
+ }, [editor]);
5430
+ const openLink = React8.useCallback(
5431
+ (target = "_blank", features = "noopener,noreferrer") => {
5432
+ if (!url) return;
5433
+ const safeUrl = sanitizeUrl(url, window.location.href);
5434
+ if (safeUrl !== "#") {
5435
+ window.open(safeUrl, target, features);
5436
+ }
5437
+ },
5438
+ [url]
5439
+ );
5440
+ return {
5441
+ url: url || "",
5442
+ setUrl,
5443
+ setLink,
5444
+ removeLink,
5445
+ openLink
5446
+ };
5447
+ }
5448
+ function useLinkState(props) {
5449
+ const { editor, hideWhenUnavailable = false } = props;
5450
+ const canSet = canSetLink(editor);
5451
+ const isActive = isLinkActive(editor);
5452
+ const [isVisible, setIsVisible] = React8.useState(false);
5453
+ React8.useEffect(() => {
5884
5454
  if (!editor) return;
5885
5455
  const handleSelectionUpdate = () => {
5886
- setIsVisible(shouldShowButton({ editor, hideWhenUnavailable }));
5456
+ setIsVisible(
5457
+ shouldShowLinkButton({
5458
+ editor,
5459
+ hideWhenUnavailable
5460
+ })
5461
+ );
5887
5462
  };
5888
5463
  handleSelectionUpdate();
5889
5464
  editor.on("selectionUpdate", handleSelectionUpdate);
@@ -5891,1078 +5466,1526 @@ function useTableBlock(config) {
5891
5466
  editor.off("selectionUpdate", handleSelectionUpdate);
5892
5467
  };
5893
5468
  }, [editor, hideWhenUnavailable]);
5894
- const handleToggle = useCallback19(() => {
5895
- if (!editor) return false;
5896
- const success = toggleTable(editor);
5897
- if (success) {
5898
- onToggled == null ? void 0 : onToggled();
5899
- }
5900
- return success;
5901
- }, [editor, onToggled]);
5902
5469
  return {
5903
5470
  isVisible,
5471
+ canSet,
5472
+ isActive
5473
+ };
5474
+ }
5475
+ function useLinkPopover(config) {
5476
+ const {
5477
+ editor: providedEditor,
5478
+ hideWhenUnavailable = false,
5479
+ onSetLink
5480
+ } = config || {};
5481
+ const { editor } = useTiptapEditor5(providedEditor);
5482
+ const { isVisible, canSet, isActive } = useLinkState({
5483
+ editor,
5484
+ hideWhenUnavailable
5485
+ });
5486
+ const linkHandler = useLinkHandler({
5487
+ editor,
5488
+ onSetLink
5489
+ });
5490
+ return {
5491
+ isVisible,
5492
+ canSet,
5904
5493
  isActive,
5905
- handleToggle,
5906
- canToggle: canToggleState,
5907
- label: intl.formatMessage(messages7.table_insert),
5908
- // shortcutKeys: CODE_BLOCK_SHORTCUT_KEY,
5909
- Icon: TableIcon
5494
+ label: "Link",
5495
+ Icon: LinkIcon,
5496
+ ...linkHandler
5910
5497
  };
5911
5498
  }
5912
5499
 
5913
- // src/ui/slash-dropdown-menu/use-slash-dropdown-menu.ts
5914
- function createSlashMenuTexts(formatMessage) {
5915
- return {
5916
- // AI
5917
- continue_writing: {
5918
- title: formatMessage(messages7.slash_continue_writing),
5919
- subtext: formatMessage(messages7.slash_continue_writing_subtext),
5920
- keywords: ["continue", "write", "continue writing", "ai"],
5921
- badge: AiSparklesIcon,
5922
- group: formatMessage(messages7.group_ai)
5923
- },
5924
- ai_ask_button: {
5925
- title: formatMessage(messages7.slash_ask_ai),
5926
- subtext: formatMessage(messages7.slash_ask_ai_subtext),
5927
- keywords: ["ai", "ask", "generate"],
5928
- badge: AiSparklesIcon,
5929
- group: formatMessage(messages7.group_ai)
5930
- },
5931
- // Style
5932
- text: {
5933
- title: formatMessage(messages7.slash_text),
5934
- subtext: formatMessage(messages7.slash_text_subtext),
5935
- keywords: ["p", "paragraph", "text"],
5936
- badge: TypeIcon,
5937
- group: formatMessage(messages7.group_style)
5938
- },
5939
- heading_1: {
5940
- title: formatMessage(messages7.slash_heading_1),
5941
- subtext: formatMessage(messages7.slash_heading_1_subtext),
5942
- keywords: ["h", "heading1", "h1"],
5943
- badge: HeadingOneIcon,
5944
- group: formatMessage(messages7.group_style)
5945
- },
5946
- heading_2: {
5947
- title: formatMessage(messages7.slash_heading_2),
5948
- subtext: formatMessage(messages7.slash_heading_2_subtext),
5949
- keywords: ["h2", "heading2", "subheading"],
5950
- badge: HeadingTwoIcon,
5951
- group: formatMessage(messages7.group_style)
5952
- },
5953
- heading_3: {
5954
- title: formatMessage(messages7.slash_heading_3),
5955
- subtext: formatMessage(messages7.slash_heading_3_subtext),
5956
- keywords: ["h3", "heading3", "subheading"],
5957
- badge: HeadingThreeIcon,
5958
- group: formatMessage(messages7.group_style)
5959
- },
5960
- bullet_list: {
5961
- title: formatMessage(messages7.slash_bullet_list),
5962
- subtext: formatMessage(messages7.slash_bullet_list_subtext),
5963
- keywords: ["ul", "li", "list", "bulletlist", "bullet list"],
5964
- badge: ListIcon2,
5965
- group: formatMessage(messages7.group_style)
5966
- },
5967
- ordered_list: {
5968
- title: formatMessage(messages7.slash_ordered_list),
5969
- subtext: formatMessage(messages7.slash_ordered_list_subtext),
5970
- keywords: ["ol", "li", "list", "numberedlist", "numbered list"],
5971
- badge: ListOrderedIcon,
5972
- group: formatMessage(messages7.group_style)
5973
- },
5974
- task_list: {
5975
- title: formatMessage(messages7.slash_task_list),
5976
- subtext: formatMessage(messages7.slash_task_list_subtext),
5977
- keywords: ["tasklist", "task list", "todo", "checklist"],
5978
- badge: ListTodoIcon,
5979
- group: formatMessage(messages7.group_style)
5980
- },
5981
- quote: {
5982
- title: formatMessage(messages7.slash_blockquote),
5983
- subtext: formatMessage(messages7.slash_blockquote_subtext),
5984
- keywords: ["quote", "blockquote"],
5985
- badge: BlockquoteIcon,
5986
- group: formatMessage(messages7.group_style)
5987
- },
5988
- code_block: {
5989
- title: formatMessage(messages7.slash_code_block),
5990
- subtext: formatMessage(messages7.slash_code_block_subtext),
5991
- keywords: ["code", "pre"],
5992
- badge: CodeBlockIcon,
5993
- group: formatMessage(messages7.group_style)
5994
- },
5995
- // Insert
5996
- control: {
5997
- title: formatMessage(messages7.slash_control),
5998
- subtext: formatMessage(messages7.slash_control_subtext),
5999
- keywords: ["control"],
6000
- badge: ControlsIcon,
6001
- group: formatMessage(messages7.group_insert)
6002
- },
6003
- divider: {
6004
- title: formatMessage(messages7.slash_separator),
6005
- subtext: formatMessage(messages7.slash_separator_subtext),
6006
- keywords: ["hr", "horizontalRule", "line", "separator"],
6007
- badge: MinusIcon,
6008
- group: formatMessage(messages7.group_insert)
6009
- },
6010
- table: {
6011
- title: formatMessage(messages7.slash_table),
6012
- subtext: formatMessage(messages7.slash_table_subtext),
6013
- keywords: ["table", "grid", "spreadsheet"],
6014
- badge: TableIcon2,
6015
- group: formatMessage(messages7.group_insert)
6016
- },
6017
- table_of_contents: {
6018
- title: formatMessage(messages7.slash_toc),
6019
- subtext: formatMessage(messages7.slash_toc_subtext),
6020
- keywords: ["toc", "table of contents", "index", "navigation", "headings"],
6021
- badge: TableOfContentsIcon,
6022
- group: formatMessage(messages7.group_insert)
6023
- },
6024
- callout: {
6025
- title: formatMessage(messages7.slash_callout),
6026
- subtext: formatMessage(messages7.slash_callout_subtext),
6027
- keywords: ["callout", "info", "warning", "alert", "note", "tip"],
6028
- badge: InfoIcon2,
6029
- group: formatMessage(messages7.group_insert)
6030
- },
6031
- callout_warning: {
6032
- title: formatMessage(messages7.slash_warning),
6033
- subtext: formatMessage(messages7.slash_warning_subtext),
6034
- keywords: ["warning", "caution", "attention"],
6035
- badge: AlertIcon2,
6036
- group: formatMessage(messages7.group_insert)
6037
- },
6038
- math: {
6039
- title: formatMessage(messages7.slash_formula),
6040
- subtext: formatMessage(messages7.slash_formula_subtext),
6041
- keywords: ["math", "latex", "formula", "equation", "katex"],
6042
- badge: TypeIcon,
6043
- group: formatMessage(messages7.group_insert)
6044
- },
6045
- // Upload
6046
- image: {
6047
- title: formatMessage(messages7.slash_image),
6048
- subtext: formatMessage(messages7.slash_image_subtext),
6049
- keywords: [
6050
- "image",
6051
- "imageUpload",
6052
- "upload",
6053
- "img",
6054
- "picture",
6055
- "media",
6056
- "url"
6057
- ],
6058
- badge: ImageIcon2,
6059
- group: formatMessage(messages7.group_upload)
5500
+ // src/ui/link-popover/link-popover.tsx
5501
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
5502
+ var LinkButton = ({ className, children, ...props }) => {
5503
+ return /* @__PURE__ */ jsx18(
5504
+ ToolbarButton2,
5505
+ {
5506
+ type: "button",
5507
+ className,
5508
+ variant: "ghost",
5509
+ color: "default",
5510
+ tabIndex: -1,
5511
+ "aria-label": "Link",
5512
+ title: "Link",
5513
+ isIconOnly: !children,
5514
+ ...props,
5515
+ children: children || /* @__PURE__ */ jsx18(LinkIcon2, {})
5516
+ }
5517
+ );
5518
+ };
5519
+ var LinkMain = ({
5520
+ url,
5521
+ setUrl,
5522
+ setLink,
5523
+ removeLink,
5524
+ openLink,
5525
+ isActive,
5526
+ onSave
5527
+ }) => {
5528
+ const [isEditing, setIsEditing] = useState17(!isActive || !url);
5529
+ useEffect17(() => {
5530
+ setIsEditing(!isActive || !url);
5531
+ }, [isActive, url]);
5532
+ const handleKeyDown = (event) => {
5533
+ if (event.key === "Enter") {
5534
+ event.preventDefault();
5535
+ setLink();
5536
+ setIsEditing(false);
5537
+ onSave == null ? void 0 : onSave();
5538
+ } else if (event.key === "Escape") {
5539
+ event.preventDefault();
5540
+ setIsEditing(false);
6060
5541
  }
6061
5542
  };
6062
- }
6063
- var getItemImplementations = () => {
6064
- return {
6065
- // AI
6066
- continue_writing: {
6067
- check: (editor) => {
6068
- const { hasContent } = hasContentAbove(editor);
6069
- const extensionsReady = isExtensionAvailable(editor, [
6070
- "ai",
6071
- "aiAdvanced"
6072
- ]);
6073
- return extensionsReady && hasContent;
6074
- },
6075
- action: ({ editor }) => {
6076
- const editorChain = editor.chain().focus();
6077
- const nodeSelectionPosition = findSelectionPosition({ editor });
6078
- if (nodeSelectionPosition !== null) {
6079
- editorChain.setNodeSelection(nodeSelectionPosition);
5543
+ const handleSave = () => {
5544
+ setLink();
5545
+ setIsEditing(false);
5546
+ onSave == null ? void 0 : onSave();
5547
+ };
5548
+ const handleEdit = () => {
5549
+ setIsEditing(true);
5550
+ };
5551
+ if (isEditing) {
5552
+ return /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1 min-w-[280px]", children: [
5553
+ /* @__PURE__ */ jsx18(
5554
+ Input3,
5555
+ {
5556
+ type: "url",
5557
+ placeholder: "Enter URL...",
5558
+ value: url,
5559
+ onChange: (e) => setUrl(e.target.value),
5560
+ onKeyDown: handleKeyDown,
5561
+ autoComplete: "off",
5562
+ autoCorrect: "off",
5563
+ autoCapitalize: "off",
5564
+ className: "flex-1 h-8 text-sm",
5565
+ autoFocus: true
6080
5566
  }
6081
- editorChain.run();
6082
- editor.chain().focus().aiGenerationShow().run();
6083
- requestAnimationFrame(() => {
6084
- const { hasContent, content } = hasContentAbove(editor);
6085
- const snippet = content.length > 500 ? `...${content.slice(-500)}` : content;
6086
- const prompt = hasContent ? `Context: ${snippet}
6087
-
6088
- Continue writing from where the text above ends. Write ONLY ONE SENTENCE. DONT REPEAT THE TEXT.` : "Start writing a new paragraph. Write ONLY ONE SENTENCE.";
6089
- editor.chain().focus().aiTextPrompt({
6090
- stream: true,
6091
- format: "rich-text",
6092
- text: prompt
6093
- }).run();
6094
- });
6095
- }
6096
- },
6097
- ai_ask_button: {
6098
- check: (editor) => isExtensionAvailable(editor, ["ai", "aiAdvanced"]),
6099
- action: ({ editor }) => {
6100
- const editorChain = editor.chain().focus();
6101
- const nodeSelectionPosition = findSelectionPosition({ editor });
6102
- if (nodeSelectionPosition !== null) {
6103
- editorChain.setNodeSelection(nodeSelectionPosition);
5567
+ ),
5568
+ /* @__PURE__ */ jsx18(
5569
+ IconButton7,
5570
+ {
5571
+ type: "button",
5572
+ size: "sm",
5573
+ variant: "ghost",
5574
+ onClick: handleSave,
5575
+ "aria-label": "Save link",
5576
+ disabled: !url,
5577
+ children: /* @__PURE__ */ jsx18(CheckIcon, { className: "size-4" })
6104
5578
  }
6105
- editorChain.run();
6106
- editor.chain().focus().aiGenerationShow().run();
6107
- }
6108
- },
6109
- // Style
6110
- text: {
6111
- check: (editor) => isNodeInSchema2("paragraph", editor),
6112
- action: ({ editor }) => {
6113
- editor.chain().focus().setParagraph().run();
6114
- }
6115
- },
6116
- heading_1: {
6117
- check: (editor) => isNodeInSchema2("heading", editor),
6118
- action: ({ editor }) => {
6119
- editor.chain().focus().toggleHeading({ level: 1 }).run();
6120
- }
6121
- },
6122
- heading_2: {
6123
- check: (editor) => isNodeInSchema2("heading", editor),
6124
- action: ({ editor }) => {
6125
- editor.chain().focus().toggleHeading({ level: 2 }).run();
6126
- }
6127
- },
6128
- heading_3: {
6129
- check: (editor) => isNodeInSchema2("heading", editor),
6130
- action: ({ editor }) => {
6131
- editor.chain().focus().toggleHeading({ level: 3 }).run();
6132
- }
6133
- },
6134
- bullet_list: {
6135
- check: (editor) => isNodeInSchema2("bulletList", editor),
6136
- action: ({ editor }) => {
6137
- editor.chain().focus().toggleBulletList().run();
6138
- }
6139
- },
6140
- ordered_list: {
6141
- check: (editor) => isNodeInSchema2("orderedList", editor),
6142
- action: ({ editor }) => {
6143
- editor.chain().focus().toggleOrderedList().run();
6144
- }
6145
- },
6146
- task_list: {
6147
- check: (editor) => isNodeInSchema2("taskList", editor),
6148
- action: ({ editor }) => {
6149
- editor.chain().focus().toggleTaskList().run();
5579
+ )
5580
+ ] });
5581
+ }
5582
+ return /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1 min-w-[280px]", children: [
5583
+ /* @__PURE__ */ jsx18(
5584
+ "a",
5585
+ {
5586
+ href: url,
5587
+ target: "_blank",
5588
+ rel: "noopener noreferrer",
5589
+ className: "flex-1 text-sm text-primary truncate max-w-[200px] hover:underline px-2",
5590
+ onClick: (e) => {
5591
+ e.preventDefault();
5592
+ openLink();
5593
+ },
5594
+ children: url
6150
5595
  }
5596
+ ),
5597
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-0.5 border-l pl-1 ml-1", children: [
5598
+ /* @__PURE__ */ jsx18(
5599
+ IconButton7,
5600
+ {
5601
+ type: "button",
5602
+ size: "sm",
5603
+ variant: "ghost",
5604
+ onClick: openLink,
5605
+ "aria-label": "Open link in new tab",
5606
+ children: /* @__PURE__ */ jsx18(ExternalLinkIcon, { className: "size-4" })
5607
+ }
5608
+ ),
5609
+ /* @__PURE__ */ jsx18(
5610
+ IconButton7,
5611
+ {
5612
+ type: "button",
5613
+ size: "sm",
5614
+ variant: "ghost",
5615
+ onClick: handleEdit,
5616
+ "aria-label": "Edit link",
5617
+ children: /* @__PURE__ */ jsx18(EditIcon2, { className: "size-4" })
5618
+ }
5619
+ ),
5620
+ /* @__PURE__ */ jsx18(
5621
+ IconButton7,
5622
+ {
5623
+ type: "button",
5624
+ size: "sm",
5625
+ variant: "ghost",
5626
+ onClick: removeLink,
5627
+ "aria-label": "Remove link",
5628
+ children: /* @__PURE__ */ jsx18(TrashIcon3, { className: "size-4" })
5629
+ }
5630
+ )
5631
+ ] })
5632
+ ] });
5633
+ };
5634
+ function LinkPopover({
5635
+ editor: providedEditor,
5636
+ hideWhenUnavailable = false,
5637
+ onSetLink,
5638
+ onOpenChange,
5639
+ autoOpenOnLinkActive = true,
5640
+ onClick,
5641
+ children,
5642
+ ...buttonProps
5643
+ }) {
5644
+ const { editor } = useTiptapEditor6(providedEditor);
5645
+ const [isOpen, setIsOpen] = useState17(false);
5646
+ const {
5647
+ isVisible,
5648
+ canSet,
5649
+ isActive,
5650
+ url,
5651
+ setUrl,
5652
+ setLink,
5653
+ removeLink,
5654
+ openLink,
5655
+ label
5656
+ } = useLinkPopover({
5657
+ editor,
5658
+ hideWhenUnavailable,
5659
+ onSetLink
5660
+ });
5661
+ const handleOnOpenChange = useCallback17(
5662
+ (nextIsOpen) => {
5663
+ setIsOpen(nextIsOpen);
5664
+ onOpenChange == null ? void 0 : onOpenChange(nextIsOpen);
6151
5665
  },
6152
- quote: {
6153
- check: (editor) => isNodeInSchema2("blockquote", editor),
6154
- action: ({ editor }) => {
6155
- editor.chain().focus().toggleBlockquote().run();
6156
- }
5666
+ [onOpenChange]
5667
+ );
5668
+ const handleSetLink = useCallback17(() => {
5669
+ setLink();
5670
+ setIsOpen(false);
5671
+ }, [setLink]);
5672
+ const handleClick = useCallback17(
5673
+ (event) => {
5674
+ onClick == null ? void 0 : onClick(event);
5675
+ if (event.defaultPrevented) return;
5676
+ setIsOpen(!isOpen);
6157
5677
  },
6158
- code_block: {
6159
- check: (editor) => isNodeInSchema2("codeBlock", editor),
6160
- action: ({ editor }) => {
6161
- editor.chain().focus().toggleNode("codeBlock", "paragraph").run();
6162
- }
6163
- },
6164
- // Insert
6165
- // mention: {
6166
- // check: (editor: Editor) =>
6167
- // isExtensionAvailable(editor, ["mention", "mentionAdvanced"]),
6168
- // action: ({ editor }: { editor: Editor }) => addMentionTrigger(editor),
6169
- // },
6170
- // emoji: {
6171
- // check: (editor: Editor) =>
6172
- // isExtensionAvailable(editor, ["emoji", "emojiPicker"]),
6173
- // action: ({ editor }: { editor: Editor }) => addEmojiTrigger(editor),
6174
- // },
6175
- divider: {
6176
- check: (editor) => isNodeInSchema2("horizontalRule", editor),
6177
- action: ({ editor }) => {
6178
- editor.chain().focus().setHorizontalRule().run();
6179
- }
6180
- },
6181
- table: {
6182
- check: (editor) => isNodeInSchema2("table", editor),
6183
- action: ({ editor }) => toggleTable(editor, { rows: 3, cols: 3, withHeaderRow: true })
6184
- },
6185
- control: {
6186
- check: (editor) => isNodeInSchema2("controlBlock", editor),
6187
- action: ({ editor }) => {
6188
- try {
6189
- return editor.chain().focus().insertControlBlock().run();
6190
- } catch (e) {
6191
- console.error(e);
6192
- }
6193
- }
6194
- },
6195
- table_of_contents: {
6196
- check: (editor) => isNodeInSchema2("tableOfContentsNode", editor),
6197
- action: ({ editor }) => {
6198
- editor.chain().focus().insertTableOfContents().run();
6199
- }
6200
- },
6201
- callout: {
6202
- check: (editor) => isNodeInSchema2("calloutNode", editor),
6203
- action: ({ editor }) => {
6204
- editor.chain().focus().insertCallout("info").run();
6205
- }
6206
- },
6207
- callout_warning: {
6208
- check: (editor) => isNodeInSchema2("calloutNode", editor),
6209
- action: ({ editor }) => {
6210
- editor.chain().focus().insertCallout("warning").run();
6211
- }
6212
- },
6213
- math: {
6214
- check: (editor) => isNodeInSchema2("mathBlock", editor),
6215
- action: ({ editor }) => {
6216
- editor.chain().focus().insertMathBlock().run();
6217
- }
6218
- },
6219
- // Upload
6220
- image: {
6221
- check: (editor) => isNodeInSchema2("imageUpload", editor),
6222
- action: ({ editor }) => {
6223
- editor.chain().focus().setImageUpload().run();
6224
- }
5678
+ [onClick, isOpen]
5679
+ );
5680
+ useEffect17(() => {
5681
+ if (autoOpenOnLinkActive && isActive) {
5682
+ setIsOpen(true);
6225
5683
  }
6226
- };
6227
- };
6228
- function organizeItemsByGroups(items, showGroups) {
6229
- if (!showGroups) {
6230
- return items.map((item) => ({ ...item, group: "" }));
5684
+ }, [autoOpenOnLinkActive, isActive]);
5685
+ if (!isVisible) {
5686
+ return null;
6231
5687
  }
6232
- const groups = {};
6233
- items.forEach((item) => {
6234
- const groupLabel = item.group || "";
6235
- if (!groups[groupLabel]) {
6236
- groups[groupLabel] = [];
6237
- }
6238
- groups[groupLabel].push(item);
6239
- });
6240
- const organizedItems = [];
6241
- Object.entries(groups).forEach(([, groupItems]) => {
6242
- organizedItems.push(...groupItems);
6243
- });
6244
- return organizedItems;
6245
- }
6246
- var ALL_SLASH_MENU_ITEMS = [
6247
- "continue_writing",
6248
- "ai_ask_button",
6249
- "text",
6250
- "heading_1",
6251
- "heading_2",
6252
- "heading_3",
6253
- "bullet_list",
6254
- "ordered_list",
6255
- "task_list",
6256
- "quote",
6257
- "code_block",
6258
- "control",
6259
- "divider",
6260
- "table",
6261
- "table_of_contents",
6262
- "callout",
6263
- "callout_warning",
6264
- "math",
6265
- "image"
6266
- ];
6267
- function useSlashDropdownMenu(config) {
6268
- const intl = useIntl12();
6269
- const getSlashMenuItems = React10.useCallback(
6270
- (editor) => {
6271
- const items = [];
6272
- const texts = createSlashMenuTexts(intl.formatMessage);
6273
- const enabledItems = (config == null ? void 0 : config.enabledItems) || ALL_SLASH_MENU_ITEMS;
6274
- const showGroups = (config == null ? void 0 : config.showGroups) !== false;
6275
- const itemImplementations = getItemImplementations();
6276
- enabledItems.forEach((itemType) => {
6277
- var _a;
6278
- const itemImpl = itemImplementations[itemType];
6279
- const itemText = texts[itemType];
6280
- if (itemImpl && itemText && itemImpl.check(editor)) {
6281
- const item = {
6282
- onSelect: ({ editor: editor2 }) => itemImpl.action({ editor: editor2 }),
6283
- ...itemText
6284
- };
6285
- if ((_a = config == null ? void 0 : config.itemGroups) == null ? void 0 : _a[itemType]) {
6286
- item.group = config.itemGroups[itemType];
6287
- } else if (!showGroups) {
6288
- item.group = "";
5688
+ return /* @__PURE__ */ jsxs14(
5689
+ Popover2.Root,
5690
+ {
5691
+ open: isOpen,
5692
+ onOpenChange: handleOnOpenChange,
5693
+ spacing: "dense",
5694
+ children: [
5695
+ /* @__PURE__ */ jsx18(
5696
+ Popover2.Trigger,
5697
+ {
5698
+ render: /* @__PURE__ */ jsx18(
5699
+ LinkButton,
5700
+ {
5701
+ "data-disabled": !canSet,
5702
+ disabled: !canSet,
5703
+ "data-active-state": isActive ? "on" : "off",
5704
+ "aria-label": label,
5705
+ "aria-pressed": isActive,
5706
+ onClick: handleClick,
5707
+ ...buttonProps
5708
+ }
5709
+ )
6289
5710
  }
6290
- items.push(item);
6291
- }
6292
- });
6293
- if (config == null ? void 0 : config.customItems) {
6294
- items.push(...config.customItems);
6295
- }
6296
- return organizeItemsByGroups(items, showGroups);
6297
- },
6298
- [config, intl]
5711
+ ),
5712
+ /* @__PURE__ */ jsx18(Popover2.Content, { children: /* @__PURE__ */ jsx18(
5713
+ LinkMain,
5714
+ {
5715
+ url,
5716
+ setUrl,
5717
+ setLink: handleSetLink,
5718
+ removeLink,
5719
+ openLink,
5720
+ isActive,
5721
+ onSave: () => setIsOpen(false)
5722
+ }
5723
+ ) })
5724
+ ]
5725
+ }
6299
5726
  );
6300
- return {
6301
- getSlashMenuItems,
6302
- config
6303
- };
6304
5727
  }
5728
+ LinkButton.displayName = "LinkButton";
6305
5729
 
6306
- // src/ui/slash-dropdown-menu/slash-dropdown-menu.tsx
6307
- import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
6308
- var SlashDropdownMenu = (props) => {
6309
- const { config, ...restProps } = props;
6310
- const { getSlashMenuItems } = useSlashDropdownMenu(config);
6311
- return /* @__PURE__ */ jsx20(
6312
- SuggestionMenu,
6313
- {
6314
- char: "/",
6315
- pluginKey: "slashDropdownMenu",
6316
- decorationClass: "tiptap-slash-decoration",
6317
- decorationContent: "Filter...",
6318
- selector: "tiptap-slash-dropdown-menu",
6319
- items: ({ query, editor }) => filterSuggestionItems(getSlashMenuItems(editor), query),
6320
- ...restProps,
6321
- children: (props2) => /* @__PURE__ */ jsx20(List, { ...props2, config })
6322
- }
6323
- );
6324
- };
6325
- var Item = (props) => {
6326
- const { item, isSelected, onSelect } = props;
6327
- const itemRef = React11.useRef(null);
6328
- React11.useEffect(() => {
6329
- const selector = document.querySelector(
6330
- '[data-selector="tiptap-slash-dropdown-menu"]'
6331
- );
6332
- if (!itemRef.current || !isSelected || !selector) return;
6333
- const overflow = getElementOverflowPosition2(itemRef.current, selector);
6334
- if (overflow === "top") {
6335
- itemRef.current.scrollIntoView(true);
6336
- } else if (overflow === "bottom") {
6337
- itemRef.current.scrollIntoView(false);
6338
- }
6339
- }, [isSelected]);
6340
- const BadgeIcon = item.badge;
6341
- return /* @__PURE__ */ jsx20(
6342
- Button6,
6343
- {
6344
- ref: itemRef,
6345
- variant: "ghost",
6346
- color: "default",
6347
- startContent: BadgeIcon && /* @__PURE__ */ jsx20(BadgeIcon, {}),
6348
- "data-active-state": isSelected ? "on" : "off",
6349
- onClick: onSelect,
6350
- fullWidth: true,
6351
- spacing: "start",
6352
- children: item.title
6353
- }
6354
- );
5730
+ // src/ui/mark-button/index.tsx
5731
+ import {
5732
+ isMarkInSchema as isMarkInSchema3,
5733
+ isNodeTypeSelected as isNodeTypeSelected2,
5734
+ useTiptapEditor as useTiptapEditor7
5735
+ } from "@kopexa/editor-utils";
5736
+ import {
5737
+ BoldIcon,
5738
+ CodeIcon,
5739
+ ItalicIcon,
5740
+ StrikeIcon,
5741
+ SubscriptIcon,
5742
+ SuperscriptIcon,
5743
+ UnderlineIcon
5744
+ } from "@kopexa/icons";
5745
+ import { ToolbarButton as ToolbarButton3 } from "@kopexa/toolbar";
5746
+ import { isNodeSelection as isNodeSelection2 } from "@tiptap/react";
5747
+ import { useCallback as useCallback18, useMemo as useMemo15 } from "react";
5748
+ import { useIntl as useIntl10 } from "react-intl";
5749
+ import { jsx as jsx19 } from "react/jsx-runtime";
5750
+ var markIcons = {
5751
+ bold: BoldIcon,
5752
+ italic: ItalicIcon,
5753
+ underline: UnderlineIcon,
5754
+ strike: StrikeIcon,
5755
+ code: CodeIcon,
5756
+ superscript: SuperscriptIcon,
5757
+ subscript: SubscriptIcon
6355
5758
  };
6356
- var List = ({
6357
- items,
6358
- selectedIndex,
6359
- onSelect,
6360
- config
6361
- }) => {
6362
- const styles = slashDropdownMenu2();
6363
- const renderedItems = React11.useMemo(() => {
6364
- const rendered = [];
6365
- const showGroups = (config == null ? void 0 : config.showGroups) !== false;
6366
- if (!showGroups) {
6367
- items.forEach((item, index) => {
6368
- rendered.push(
6369
- /* @__PURE__ */ jsx20(
6370
- Item,
6371
- {
6372
- item,
6373
- isSelected: index === selectedIndex,
6374
- onSelect: () => onSelect(item)
6375
- },
6376
- `item-${index}-${item.title}`
6377
- )
6378
- );
6379
- });
6380
- return rendered;
5759
+ var markShortcutKeys = {
5760
+ bold: "mod+b",
5761
+ italic: "mod+i",
5762
+ underline: "mod+u",
5763
+ strike: "mod+shift+s",
5764
+ code: "mod+e",
5765
+ superscript: "mod+.",
5766
+ subscript: "mod+,"
5767
+ };
5768
+ function canToggleMark(editor, type) {
5769
+ if (!editor || !editor.isEditable) return false;
5770
+ if (!isMarkInSchema3(type, editor) || isNodeTypeSelected2(editor, ["image"]))
5771
+ return false;
5772
+ return editor.can().toggleMark(type);
5773
+ }
5774
+ function isMarkActive(editor, type) {
5775
+ if (!editor) return false;
5776
+ return editor.isActive(type);
5777
+ }
5778
+ function toggleMark(editor, type) {
5779
+ if (!editor) return;
5780
+ editor.chain().focus().toggleMark(type).run();
5781
+ }
5782
+ function isMarkButtonDisabled(editor, type, userDisabled = false) {
5783
+ if (!editor) return true;
5784
+ if (userDisabled) return true;
5785
+ if (editor.isActive("codeBlock")) return true;
5786
+ if (!canToggleMark(editor, type)) return true;
5787
+ return false;
5788
+ }
5789
+ function shouldShowMarkButton(params) {
5790
+ const { editor, type, hideWhenUnavailable, markInSchema } = params;
5791
+ if (!markInSchema || !editor) {
5792
+ return false;
5793
+ }
5794
+ if (hideWhenUnavailable) {
5795
+ if (isNodeSelection2(editor.state.selection) || !canToggleMark(editor, type)) {
5796
+ return false;
6381
5797
  }
6382
- const groups = {};
6383
- items.forEach((item, index) => {
6384
- const groupLabel = item.group || "";
6385
- if (!groups[groupLabel]) {
6386
- groups[groupLabel] = { items: [], indices: [] };
6387
- }
6388
- groups[groupLabel].items.push(item);
6389
- groups[groupLabel].indices.push(index);
6390
- });
6391
- Object.entries(groups).forEach(([groupLabel, groupData], groupIndex) => {
6392
- if (groupIndex > 0) {
6393
- rendered.push(
6394
- /* @__PURE__ */ jsx20(
6395
- Separator,
6396
- {
6397
- orientation: "horizontal"
6398
- },
6399
- `separator-${groupIndex.toString()}`
6400
- )
6401
- );
6402
- }
6403
- const groupItems = groupData.items.map((item, itemIndex) => {
6404
- const originalIndex = groupData.indices[itemIndex];
6405
- return /* @__PURE__ */ jsx20(
6406
- Item,
6407
- {
6408
- item,
6409
- isSelected: originalIndex === selectedIndex,
6410
- onSelect: () => onSelect(item)
6411
- },
6412
- `item-${originalIndex}-${item.title}`
6413
- );
6414
- });
6415
- if (groupLabel) {
6416
- rendered.push(
6417
- /* @__PURE__ */ jsxs15(
6418
- "div",
6419
- {
6420
- className: styles.cardItemGroup(),
6421
- children: [
6422
- /* @__PURE__ */ jsx20("div", { className: styles.cardGroupLabel(), children: groupLabel }),
6423
- /* @__PURE__ */ jsx20("div", { className: styles.cardGroup(), children: groupItems })
6424
- ]
6425
- },
6426
- `group-${groupIndex}-${groupLabel}`
6427
- )
6428
- );
6429
- } else {
6430
- rendered.push(...groupItems);
5798
+ }
5799
+ return true;
5800
+ }
5801
+ var markMessages = {
5802
+ bold: messages7.bold,
5803
+ italic: messages7.italic,
5804
+ underline: messages7.underline,
5805
+ strike: messages7.strikethrough,
5806
+ code: messages7.code,
5807
+ superscript: messages7.superscript,
5808
+ subscript: messages7.subscript
5809
+ };
5810
+ function getTranslatedMarkName(type, intl) {
5811
+ return intl.formatMessage(markMessages[type]);
5812
+ }
5813
+ function useMarkState(editor, type, disabled = false) {
5814
+ const intl = useIntl10();
5815
+ const markInSchema = isMarkInSchema3(type, editor);
5816
+ const isDisabled = isMarkButtonDisabled(editor, type, disabled);
5817
+ const isActive = isMarkActive(editor, type);
5818
+ const Icon = markIcons[type];
5819
+ const shortcutKey = markShortcutKeys[type];
5820
+ const formattedName = getTranslatedMarkName(type, intl);
5821
+ return {
5822
+ markInSchema,
5823
+ isDisabled,
5824
+ isActive,
5825
+ Icon,
5826
+ shortcutKey,
5827
+ formattedName
5828
+ };
5829
+ }
5830
+ var MarkButton = ({
5831
+ editor: providedEditor,
5832
+ type,
5833
+ text,
5834
+ hideWhenUnavailable = false,
5835
+ className = "",
5836
+ disabled,
5837
+ onClick,
5838
+ children,
5839
+ ...buttonProps
5840
+ }) => {
5841
+ const { editor } = useTiptapEditor7(providedEditor);
5842
+ const {
5843
+ markInSchema,
5844
+ isDisabled,
5845
+ isActive,
5846
+ Icon,
5847
+ shortcutKey,
5848
+ formattedName
5849
+ } = useMarkState(editor, type, disabled);
5850
+ const handleClick = useCallback18(
5851
+ (e) => {
5852
+ onClick == null ? void 0 : onClick(e);
5853
+ if (!e.defaultPrevented && !isDisabled && editor) {
5854
+ toggleMark(editor, type);
6431
5855
  }
5856
+ },
5857
+ [onClick, isDisabled, editor, type]
5858
+ );
5859
+ const show = useMemo15(() => {
5860
+ return shouldShowMarkButton({
5861
+ editor,
5862
+ type,
5863
+ hideWhenUnavailable,
5864
+ markInSchema
6432
5865
  });
6433
- return rendered;
6434
- }, [items, selectedIndex, onSelect, config == null ? void 0 : config.showGroups, styles]);
6435
- if (!renderedItems.length) {
5866
+ }, [editor, type, hideWhenUnavailable, markInSchema]);
5867
+ if (!show || !editor || !editor.isEditable) {
6436
5868
  return null;
6437
5869
  }
6438
- return /* @__PURE__ */ jsx20(
6439
- "div",
5870
+ return /* @__PURE__ */ jsx19(
5871
+ ToolbarButton3,
6440
5872
  {
6441
- className: styles.card(),
6442
- style: {
6443
- maxHeight: "var(--suggestion-menu-max-height)"
6444
- },
6445
- children: /* @__PURE__ */ jsx20("div", { className: styles.body(), children: renderedItems })
5873
+ type: "button",
5874
+ className: className.trim(),
5875
+ disabled: isDisabled,
5876
+ variant: "ghost",
5877
+ color: "default",
5878
+ "data-active-state": isActive ? "on" : "off",
5879
+ "data-disabled": isDisabled,
5880
+ tabIndex: -1,
5881
+ "aria-label": formattedName,
5882
+ "aria-pressed": isActive,
5883
+ title: formattedName,
5884
+ shortcutKeys: shortcutKey,
5885
+ onClick: handleClick,
5886
+ isIconOnly: true,
5887
+ ...buttonProps,
5888
+ children: /* @__PURE__ */ jsx19(Icon, {})
6446
5889
  }
6447
5890
  );
6448
5891
  };
6449
5892
 
6450
- // src/presets/basic/editor-header.tsx
6451
- import { useTiptapEditor as useTiptapEditor26 } from "@kopexa/editor-utils";
6452
- import { MoreVerticalIcon } from "@kopexa/icons";
6453
- import { Popover as Popover3 } from "@kopexa/popover";
6454
- import {
6455
- Toolbar as Toolbar2,
6456
- ToolbarButton as ToolbarButton8,
6457
- ToolbarGroup as ToolbarGroup2,
6458
- ToolbarSeparator as ToolbarSeparator3
6459
- } from "@kopexa/toolbar";
6460
- import { useIsMobile as useIsMobile3 } from "@kopexa/use-is-mobile";
6461
- import { useEffect as useEffect31, useRef as useRef12, useState as useState30 } from "react";
6462
- import { useIntl as useIntl18 } from "react-intl";
6463
-
6464
- // src/hooks/use-cursor-visibility.ts
6465
- import * as React13 from "react";
6466
-
6467
- // src/hooks/use-window-size.ts
6468
- import * as React12 from "react";
6469
- function useWindowSize() {
6470
- const [windowSize, setWindowSize] = React12.useState({
6471
- width: 0,
6472
- height: 0,
6473
- offsetTop: 0
6474
- });
6475
- React12.useEffect(() => {
6476
- handleResize();
6477
- function handleResize() {
6478
- if (typeof window === "undefined") return;
6479
- const vp = window.visualViewport;
6480
- if (!vp) return;
6481
- const { width = 0, height = 0, offsetTop = 0 } = vp;
6482
- setWindowSize((state) => {
6483
- if (width === state.width && height === state.height && offsetTop === state.offsetTop) {
6484
- return state;
6485
- }
6486
- return { width, height, offsetTop };
6487
- });
6488
- }
6489
- const visualViewport = window.visualViewport;
6490
- if (visualViewport) {
6491
- visualViewport.addEventListener("resize", handleResize);
6492
- visualViewport.addEventListener("scroll", handleResize);
5893
+ // src/ui/bubble-menu/index.tsx
5894
+ import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
5895
+ function useResolvedHighlightColors(editor) {
5896
+ var _a;
5897
+ if (!((_a = editor == null ? void 0 : editor.view) == null ? void 0 : _a.dom)) return void 0;
5898
+ const editorEl = editor.view.dom;
5899
+ const computed = getComputedStyle(editorEl);
5900
+ return HIGHLIGHT_COLORS.filter((c) => c.value !== "var(--tt-bg-color)").map(
5901
+ (color) => {
5902
+ const resolveVar = (v) => {
5903
+ const match = v.match(/^var\((.+)\)$/);
5904
+ if (!match) return v;
5905
+ return computed.getPropertyValue(match[1]).trim() || v;
5906
+ };
5907
+ return {
5908
+ ...color,
5909
+ value: resolveVar(color.value),
5910
+ border: resolveVar(color.border)
5911
+ };
6493
5912
  }
6494
- return () => {
6495
- if (visualViewport) {
6496
- visualViewport.removeEventListener("resize", handleResize);
6497
- visualViewport.removeEventListener("scroll", handleResize);
6498
- }
6499
- };
6500
- }, []);
6501
- return windowSize;
5913
+ );
6502
5914
  }
6503
-
6504
- // src/hooks/use-cursor-visibility.ts
6505
- function useCursorVisibility({
6506
- editor,
6507
- overlayHeight = 0,
6508
- elementRef = null
6509
- }) {
6510
- const { height: windowHeight } = useWindowSize();
6511
- const [rect, setRect] = React13.useState({
6512
- x: 0,
6513
- y: 0,
6514
- width: 0,
6515
- height: 0
6516
- });
6517
- const updateRect = React13.useCallback(() => {
6518
- var _a;
6519
- const element = (_a = elementRef == null ? void 0 : elementRef.current) != null ? _a : document.body;
6520
- const { x, y, width, height } = element.getBoundingClientRect();
6521
- setRect({ x, y, width, height });
6522
- }, [elementRef]);
6523
- React13.useEffect(() => {
6524
- var _a;
6525
- const element = (_a = elementRef == null ? void 0 : elementRef.current) != null ? _a : document.body;
6526
- updateRect();
6527
- const resizeObserver = new ResizeObserver(() => {
6528
- window.requestAnimationFrame(updateRect);
6529
- });
6530
- resizeObserver.observe(element);
6531
- window.addEventListener("scroll", updateRect, { passive: true });
6532
- return () => {
6533
- resizeObserver.disconnect();
6534
- window.removeEventListener("scroll", updateRect);
6535
- };
6536
- }, [elementRef, updateRect]);
6537
- React13.useEffect(() => {
6538
- const ensureCursorVisibility = () => {
6539
- if (!editor) return;
6540
- const { state, view } = editor;
6541
- if (!view.hasFocus()) return;
6542
- const { from } = state.selection;
6543
- const cursorCoords = view.coordsAtPos(from);
6544
- if (windowHeight < rect.height) {
6545
- if (cursorCoords) {
6546
- const availableSpace = windowHeight - cursorCoords.top - overlayHeight > 0;
6547
- if (!availableSpace) {
6548
- const targetScrollY = (
6549
- // TODO: Needed?
6550
- // window.scrollY + (cursorCoords.top - windowHeight / 2)
6551
- cursorCoords.top - windowHeight / 2
6552
- );
6553
- window.scrollTo({
6554
- top: targetScrollY,
6555
- behavior: "smooth"
6556
- });
6557
- }
6558
- }
6559
- }
6560
- };
6561
- ensureCursorVisibility();
6562
- }, [editor, overlayHeight, windowHeight, rect.height]);
6563
- return rect;
5915
+ function BubbleMenu({ editor }) {
5916
+ const resolvedColors = useResolvedHighlightColors(editor);
5917
+ if (!editor) {
5918
+ return null;
5919
+ }
5920
+ return /* @__PURE__ */ jsx20(
5921
+ TiptapBubbleMenu,
5922
+ {
5923
+ editor,
5924
+ shouldShow: ({ editor: e, state, view }) => {
5925
+ const { selection } = state;
5926
+ const { empty } = selection;
5927
+ if (!view.hasFocus()) return false;
5928
+ if (empty) return false;
5929
+ if (e.isActive("codeBlock")) return false;
5930
+ if (e.isActive("link")) return false;
5931
+ if (e.isActive("variable")) return false;
5932
+ if (!e.isEditable) return false;
5933
+ return true;
5934
+ },
5935
+ options: {
5936
+ placement: "top",
5937
+ offset: 8
5938
+ },
5939
+ className: "rounded-lg border bg-background shadow-md",
5940
+ children: /* @__PURE__ */ jsxs15(Toolbar, { radius: "md", border: "none", className: "p-1", children: [
5941
+ /* @__PURE__ */ jsxs15(ToolbarGroup, { children: [
5942
+ /* @__PURE__ */ jsx20(MarkButton, { type: "bold" }),
5943
+ /* @__PURE__ */ jsx20(MarkButton, { type: "italic" }),
5944
+ /* @__PURE__ */ jsx20(MarkButton, { type: "underline" }),
5945
+ /* @__PURE__ */ jsx20(MarkButton, { type: "strike" }),
5946
+ /* @__PURE__ */ jsx20(MarkButton, { type: "code" })
5947
+ ] }),
5948
+ /* @__PURE__ */ jsx20(ToolbarSeparator2, {}),
5949
+ /* @__PURE__ */ jsxs15(ToolbarGroup, { children: [
5950
+ /* @__PURE__ */ jsx20(ColorHighlightPopover, { colors: resolvedColors }),
5951
+ /* @__PURE__ */ jsx20(LinkPopover, { autoOpenOnLinkActive: false })
5952
+ ] })
5953
+ ] })
5954
+ }
5955
+ );
6564
5956
  }
6565
5957
 
6566
- // src/ui/color-highlight-popover/color-highlight-popover.tsx
6567
- import { IconButton as IconButton8 } from "@kopexa/button";
6568
- import { useTiptapEditor as useTiptapEditor9 } from "@kopexa/editor-utils";
6569
- import { BanIcon, HighlighterIcon as HighlighterIcon2 } from "@kopexa/icons";
6570
- import { Popover as Popover2 } from "@kopexa/popover";
6571
- import { ToolbarSeparator as ToolbarSeparator2 } from "@kopexa/toolbar";
6572
- import { useMemo as useMemo16, useRef as useRef11, useState as useState22 } from "react";
6573
-
6574
- // src/ui/color-highlight-button/color-highlight-button.tsx
6575
- import { useTiptapEditor as useTiptapEditor8 } from "@kopexa/editor-utils";
6576
- import { colorHighlightButton } from "@kopexa/theme";
6577
- import { ToolbarButton as ToolbarButton3 } from "@kopexa/toolbar";
6578
- import { useCallback as useCallback23, useMemo as useMemo15 } from "react";
5958
+ // src/ui/copy-anchor-link-button/use-scroll-to-hash.ts
5959
+ import { getEditorExtension, useTiptapEditor as useTiptapEditor8 } from "@kopexa/editor-utils";
5960
+ import * as React10 from "react";
6579
5961
 
6580
- // src/ui/color-highlight-button/use-color-highlight.ts
6581
- import {
6582
- isMarkInSchema as isMarkInSchema3,
6583
- isNodeTypeSelected as isNodeTypeSelected3,
6584
- useTiptapEditor as useTiptapEditor7
6585
- } from "@kopexa/editor-utils";
6586
- import { HighlighterIcon } from "@kopexa/icons";
6587
- import { useIsMobile } from "@kopexa/use-is-mobile";
6588
- import * as React14 from "react";
6589
- import { useHotkeys } from "react-hotkeys-hook";
6590
- import { useIntl as useIntl13 } from "react-intl";
6591
- var COLOR_HIGHLIGHT_SHORTCUT_KEY = "mod+shift+h";
6592
- var HIGHLIGHT_COLORS = [
6593
- {
6594
- label: "Default background",
6595
- value: "var(--tt-bg-color)",
6596
- border: "var(--tt-bg-color-contrast)"
6597
- },
6598
- {
6599
- label: "Gray background",
6600
- value: "var(--tt-color-highlight-gray)",
6601
- border: "var(--tt-color-highlight-gray-contrast)"
6602
- },
6603
- {
6604
- label: "Brown background",
6605
- value: "var(--tt-color-highlight-brown)",
6606
- border: "var(--tt-color-highlight-brown-contrast)"
6607
- },
6608
- {
6609
- label: "Orange background",
6610
- value: "var(--tt-color-highlight-orange)",
6611
- border: "var(--tt-color-highlight-orange-contrast)"
6612
- },
6613
- {
6614
- label: "Yellow background",
6615
- value: "var(--tt-color-highlight-yellow)",
6616
- border: "var(--tt-color-highlight-yellow-contrast)"
6617
- },
6618
- {
6619
- label: "Green background",
6620
- value: "var(--tt-color-highlight-green)",
6621
- border: "var(--tt-color-highlight-green-contrast)"
6622
- },
6623
- {
6624
- label: "Blue background",
6625
- value: "var(--tt-color-highlight-blue)",
6626
- border: "var(--tt-color-highlight-blue-contrast)"
6627
- },
6628
- {
6629
- label: "Purple background",
6630
- value: "var(--tt-color-highlight-purple)",
6631
- border: "var(--tt-color-highlight-purple-contrast)"
6632
- },
6633
- {
6634
- label: "Pink background",
6635
- value: "var(--tt-color-highlight-pink)",
6636
- border: "var(--tt-color-highlight-pink-contrast)"
6637
- },
6638
- {
6639
- label: "Red background",
6640
- value: "var(--tt-color-highlight-red)",
6641
- border: "var(--tt-color-highlight-red-contrast)"
6642
- }
6643
- ];
6644
- function pickHighlightColorsByValue(values) {
6645
- const colorMap = new Map(
6646
- HIGHLIGHT_COLORS.map((color) => [color.value, color])
5962
+ // src/hooks/use-floating-toolbar-visibility.ts
5963
+ import { NodeSelection } from "@tiptap/pm/state";
5964
+ import { isNodeSelection as isNodeSelection3 } from "@tiptap/react";
5965
+ import * as React9 from "react";
5966
+ var HIDE_FLOATING_META = "hideFloatingToolbar";
5967
+ var selectNodeAndHideFloating = (editor, pos) => {
5968
+ if (!editor) return;
5969
+ const { state, view } = editor;
5970
+ view.dispatch(
5971
+ state.tr.setSelection(NodeSelection.create(state.doc, pos)).setMeta(HIDE_FLOATING_META, true)
6647
5972
  );
6648
- return values.map((value) => colorMap.get(value)).filter((color) => !!color);
6649
- }
6650
- function canColorHighlight(editor) {
6651
- if (!editor || !editor.isEditable) return false;
6652
- if (!isMarkInSchema3("highlight", editor) || isNodeTypeSelected3(editor, ["image"]))
6653
- return false;
6654
- return editor.can().setMark("highlight");
6655
- }
6656
- function isColorHighlightActive(editor, highlightColor) {
6657
- if (!editor || !editor.isEditable) return false;
6658
- return highlightColor ? editor.isActive("highlight", { color: highlightColor }) : editor.isActive("highlight");
6659
- }
6660
- function removeHighlight(editor) {
6661
- if (!editor || !editor.isEditable) return false;
6662
- if (!canColorHighlight(editor)) return false;
6663
- return editor.chain().focus().unsetMark("highlight").run();
6664
- }
6665
- function shouldShowButton2(props) {
6666
- const { editor, hideWhenUnavailable } = props;
6667
- if (!editor || !editor.isEditable) return false;
6668
- if (!isMarkInSchema3("highlight", editor)) return false;
6669
- if (hideWhenUnavailable && !editor.isActive("code")) {
6670
- return canColorHighlight(editor);
6671
- }
6672
- return true;
6673
- }
6674
- function useColorHighlight(config) {
5973
+ };
5974
+
5975
+ // src/ui/copy-anchor-link-button/use-scroll-to-hash.ts
5976
+ function useScrollToHash(config = {}) {
6675
5977
  const {
6676
5978
  editor: providedEditor,
6677
- label,
6678
- highlightColor,
6679
- hideWhenUnavailable = false,
6680
- onApplied
5979
+ onTargetFound = () => {
5980
+ },
5981
+ onTargetNotFound = () => {
5982
+ }
6681
5983
  } = config;
6682
- const intl = useIntl13();
6683
- const { editor } = useTiptapEditor7(providedEditor);
6684
- const isMobile = useIsMobile();
6685
- const [isVisible, setIsVisible] = React14.useState(true);
6686
- const canColorHighlightState = canColorHighlight(editor);
6687
- const isActive = isColorHighlightActive(editor, highlightColor);
6688
- React14.useEffect(() => {
5984
+ const { editor } = useTiptapEditor8(providedEditor);
5985
+ const scrollToNode = React10.useCallback(
5986
+ (id) => {
5987
+ var _a, _b, _c;
5988
+ if (!editor) return false;
5989
+ const attributeName = (_c = (_b = (_a = getEditorExtension(editor, "uniqueID")) == null ? void 0 : _a.options) == null ? void 0 : _b.attributeName) != null ? _c : "data-id";
5990
+ let position = null;
5991
+ editor.state.doc.descendants((node, pos) => {
5992
+ var _a2;
5993
+ if (((_a2 = node.attrs) == null ? void 0 : _a2[attributeName]) === id) {
5994
+ position = pos;
5995
+ return false;
5996
+ }
5997
+ return true;
5998
+ });
5999
+ if (position === null) return false;
6000
+ selectNodeAndHideFloating(editor, position);
6001
+ setTimeout(() => {
6002
+ let dom = null;
6003
+ if (typeof position === "number") {
6004
+ dom = editor.view.nodeDOM(position);
6005
+ }
6006
+ if (dom) {
6007
+ dom.scrollIntoView({ behavior: "smooth", block: "center" });
6008
+ }
6009
+ }, 0);
6010
+ return true;
6011
+ },
6012
+ [editor]
6013
+ );
6014
+ const handleScroll = React10.useCallback(
6015
+ (delay = 0) => {
6016
+ var _a;
6017
+ const hash = (_a = window.location.hash) == null ? void 0 : _a.substring(1);
6018
+ if (!hash) return;
6019
+ setTimeout(() => {
6020
+ if (scrollToNode(hash)) {
6021
+ onTargetFound(hash);
6022
+ } else {
6023
+ onTargetNotFound(hash);
6024
+ }
6025
+ }, delay);
6026
+ },
6027
+ [scrollToNode, onTargetFound, onTargetNotFound]
6028
+ );
6029
+ React10.useEffect(() => {
6030
+ var _a, _b;
6689
6031
  if (!editor) return;
6690
- const handleSelectionUpdate = () => {
6691
- setIsVisible(shouldShowButton2({ editor, hideWhenUnavailable }));
6692
- };
6032
+ const provider = (_b = (_a = editor.extensionManager.extensions.find(
6033
+ (ext) => ext.name === "collaborationCaret"
6034
+ )) == null ? void 0 : _a.options) == null ? void 0 : _b.provider;
6035
+ if (provider == null ? void 0 : provider.on) {
6036
+ const syncHandler = () => handleScroll(500);
6037
+ provider.on("synced", syncHandler);
6038
+ return () => {
6039
+ var _a2;
6040
+ (_a2 = provider.off) == null ? void 0 : _a2.call(provider, "synced", syncHandler);
6041
+ };
6042
+ } else {
6043
+ handleScroll(500);
6044
+ }
6045
+ }, [editor, handleScroll]);
6046
+ React10.useEffect(() => {
6047
+ const immediateScroll = () => handleScroll();
6048
+ const delayedScroll = () => handleScroll(500);
6049
+ window.addEventListener("hashchange", immediateScroll);
6050
+ window.addEventListener("pageshow", delayedScroll);
6051
+ window.addEventListener("popstate", immediateScroll);
6052
+ return () => {
6053
+ window.removeEventListener("hashchange", immediateScroll);
6054
+ window.removeEventListener("pageshow", delayedScroll);
6055
+ window.removeEventListener("popstate", immediateScroll);
6056
+ };
6057
+ }, [handleScroll]);
6058
+ return { scrollToHash: scrollToNode };
6059
+ }
6060
+
6061
+ // src/ui/link-bubble/index.tsx
6062
+ import { IconButton as IconButton8 } from "@kopexa/button";
6063
+ import { EditIcon as EditIcon3, ExternalLinkIcon as ExternalLinkIcon2, TrashIcon as TrashIcon4 } from "@kopexa/icons";
6064
+ import { Input as Input4 } from "@kopexa/input";
6065
+ import { BubbleMenu as TiptapBubbleMenu2 } from "@tiptap/react/menus";
6066
+ import { useCallback as useCallback20, useEffect as useEffect20, useState as useState19 } from "react";
6067
+ import { useIntl as useIntl11 } from "react-intl";
6068
+ import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
6069
+ function LinkBubble({ editor }) {
6070
+ const intl = useIntl11();
6071
+ const [isEditing, setIsEditing] = useState19(false);
6072
+ const [url, setUrl] = useState19("");
6073
+ const getCurrentUrl = useCallback20(() => {
6074
+ if (!editor) return "";
6075
+ const attrs = editor.getAttributes("link");
6076
+ return attrs.href || "";
6077
+ }, [editor]);
6078
+ useEffect20(() => {
6079
+ const isLinkActive2 = editor == null ? void 0 : editor.isActive("link");
6080
+ if (isLinkActive2) {
6081
+ setUrl(getCurrentUrl());
6082
+ setIsEditing(false);
6083
+ }
6084
+ }, [editor, getCurrentUrl]);
6085
+ const handleOpenLink = useCallback20(() => {
6086
+ const href = getCurrentUrl();
6087
+ if (href) {
6088
+ window.open(href, "_blank", "noopener,noreferrer");
6089
+ }
6090
+ }, [getCurrentUrl]);
6091
+ const handleRemoveLink = useCallback20(() => {
6092
+ editor == null ? void 0 : editor.chain().focus().unsetLink().run();
6093
+ }, [editor]);
6094
+ const handleEdit = useCallback20(() => {
6095
+ setUrl(getCurrentUrl());
6096
+ setIsEditing(true);
6097
+ }, [getCurrentUrl]);
6098
+ const handleSave = useCallback20(() => {
6099
+ if (url) {
6100
+ editor == null ? void 0 : editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
6101
+ } else {
6102
+ editor == null ? void 0 : editor.chain().focus().unsetLink().run();
6103
+ }
6104
+ setIsEditing(false);
6105
+ }, [editor, url]);
6106
+ const handleKeyDown = useCallback20(
6107
+ (e) => {
6108
+ if (e.key === "Enter") {
6109
+ e.preventDefault();
6110
+ handleSave();
6111
+ } else if (e.key === "Escape") {
6112
+ e.preventDefault();
6113
+ setIsEditing(false);
6114
+ setUrl(getCurrentUrl());
6115
+ }
6116
+ },
6117
+ [handleSave, getCurrentUrl]
6118
+ );
6119
+ if (!editor) {
6120
+ return null;
6121
+ }
6122
+ return /* @__PURE__ */ jsx21(
6123
+ TiptapBubbleMenu2,
6124
+ {
6125
+ editor,
6126
+ pluginKey: "linkBubbleMenu",
6127
+ shouldShow: ({ editor: e, view }) => {
6128
+ if (!view.hasFocus()) return false;
6129
+ return e.isActive("link") && e.isEditable;
6130
+ },
6131
+ options: {
6132
+ placement: "bottom-start",
6133
+ offset: 8
6134
+ },
6135
+ className: "rounded-lg border bg-background shadow-md",
6136
+ children: /* @__PURE__ */ jsx21("div", { className: "flex items-center gap-1 p-1.5 min-w-[280px]", children: isEditing ? /* @__PURE__ */ jsxs16(Fragment4, { children: [
6137
+ /* @__PURE__ */ jsx21(
6138
+ Input4,
6139
+ {
6140
+ type: "url",
6141
+ value: url,
6142
+ onChange: (e) => setUrl(e.target.value),
6143
+ onKeyDown: handleKeyDown,
6144
+ placeholder: intl.formatMessage(messages7.link_placeholder),
6145
+ className: "flex-1 h-8 text-sm",
6146
+ autoFocus: true
6147
+ }
6148
+ ),
6149
+ /* @__PURE__ */ jsx21(
6150
+ IconButton8,
6151
+ {
6152
+ type: "button",
6153
+ size: "sm",
6154
+ variant: "ghost",
6155
+ onClick: handleSave,
6156
+ "aria-label": intl.formatMessage(messages7.link_save),
6157
+ children: /* @__PURE__ */ jsx21(EditIcon3, { className: "size-4" })
6158
+ }
6159
+ )
6160
+ ] }) : /* @__PURE__ */ jsxs16(Fragment4, { children: [
6161
+ /* @__PURE__ */ jsx21(
6162
+ "a",
6163
+ {
6164
+ href: getCurrentUrl(),
6165
+ target: "_blank",
6166
+ rel: "noopener noreferrer",
6167
+ className: "flex-1 text-sm text-primary truncate max-w-[200px] hover:underline px-2",
6168
+ onClick: (e) => {
6169
+ e.preventDefault();
6170
+ handleOpenLink();
6171
+ },
6172
+ children: getCurrentUrl()
6173
+ }
6174
+ ),
6175
+ /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-0.5 border-l pl-1 ml-1", children: [
6176
+ /* @__PURE__ */ jsx21(
6177
+ IconButton8,
6178
+ {
6179
+ type: "button",
6180
+ size: "sm",
6181
+ variant: "ghost",
6182
+ onClick: handleOpenLink,
6183
+ "aria-label": intl.formatMessage(messages7.link_open),
6184
+ children: /* @__PURE__ */ jsx21(ExternalLinkIcon2, { className: "size-4" })
6185
+ }
6186
+ ),
6187
+ /* @__PURE__ */ jsx21(
6188
+ IconButton8,
6189
+ {
6190
+ type: "button",
6191
+ size: "sm",
6192
+ variant: "ghost",
6193
+ onClick: handleEdit,
6194
+ "aria-label": intl.formatMessage(messages7.link_edit),
6195
+ children: /* @__PURE__ */ jsx21(EditIcon3, { className: "size-4" })
6196
+ }
6197
+ ),
6198
+ /* @__PURE__ */ jsx21(
6199
+ IconButton8,
6200
+ {
6201
+ type: "button",
6202
+ size: "sm",
6203
+ variant: "ghost",
6204
+ onClick: handleRemoveLink,
6205
+ "aria-label": intl.formatMessage(messages7.link_remove),
6206
+ children: /* @__PURE__ */ jsx21(TrashIcon4, { className: "size-4" })
6207
+ }
6208
+ )
6209
+ ] })
6210
+ ] }) })
6211
+ }
6212
+ );
6213
+ }
6214
+
6215
+ // src/ui/slash-dropdown-menu/slash-dropdown-menu.tsx
6216
+ import { Button as Button6 } from "@kopexa/button";
6217
+ import { getElementOverflowPosition as getElementOverflowPosition2 } from "@kopexa/editor-utils";
6218
+ import { Separator } from "@kopexa/separator";
6219
+ import { slashDropdownMenu as slashDropdownMenu2 } from "@kopexa/theme";
6220
+ import * as React12 from "react";
6221
+
6222
+ // src/ui/slash-dropdown-menu/use-slash-dropdown-menu.ts
6223
+ import {
6224
+ findSelectionPosition,
6225
+ hasContentAbove,
6226
+ isExtensionAvailable,
6227
+ isNodeInSchema as isNodeInSchema2
6228
+ } from "@kopexa/editor-utils";
6229
+ import {
6230
+ AiSparklesIcon,
6231
+ AlertIcon as AlertIcon2,
6232
+ BlockquoteIcon,
6233
+ CodeBlockIcon,
6234
+ ControlsIcon,
6235
+ HeadingOneIcon,
6236
+ HeadingThreeIcon,
6237
+ HeadingTwoIcon,
6238
+ ImageIcon as ImageIcon2,
6239
+ InfoIcon as InfoIcon2,
6240
+ ListIcon as ListIcon2,
6241
+ ListOrderedIcon,
6242
+ ListTodoIcon,
6243
+ MinusIcon,
6244
+ TableIcon as TableIcon2,
6245
+ TableOfContentsIcon,
6246
+ TypeIcon
6247
+ } from "@kopexa/icons";
6248
+ import * as React11 from "react";
6249
+ import { useIntl as useIntl13 } from "react-intl";
6250
+
6251
+ // src/ui/table-button/use-table.ts
6252
+ import {
6253
+ isNodeInSchema,
6254
+ isNodeTypeSelected as isNodeTypeSelected3,
6255
+ useTiptapEditor as useTiptapEditor9
6256
+ } from "@kopexa/editor-utils";
6257
+ import { TableIcon } from "@kopexa/icons";
6258
+ import { isNodeSelection as isNodeSelection4 } from "@tiptap/react";
6259
+ import { useCallback as useCallback21, useEffect as useEffect21, useState as useState20 } from "react";
6260
+ import { useIntl as useIntl12 } from "react-intl";
6261
+ function canToggle(editor) {
6262
+ if (!editor || !editor.isEditable) return false;
6263
+ if (!isNodeInSchema("table", editor) || isNodeTypeSelected3(editor, ["image"])) {
6264
+ return false;
6265
+ }
6266
+ try {
6267
+ return editor.can().insertTable({ rows: 3, cols: 3, withHeaderRow: true });
6268
+ } catch {
6269
+ return false;
6270
+ }
6271
+ }
6272
+ function toggleTable(editor, config) {
6273
+ var _a;
6274
+ if (!editor || !editor.isEditable) return false;
6275
+ if (!canToggle(editor)) return false;
6276
+ try {
6277
+ return editor.chain().focus().insertTable({
6278
+ rows: (config == null ? void 0 : config.rows) || 3,
6279
+ cols: (config == null ? void 0 : config.cols) || 3,
6280
+ withHeaderRow: (_a = config == null ? void 0 : config.withHeaderRow) != null ? _a : true
6281
+ }).run();
6282
+ } catch {
6283
+ return false;
6284
+ }
6285
+ }
6286
+ function shouldShowButton2(props) {
6287
+ const { editor, hideWhenUnavailable } = props;
6288
+ if (!editor || !editor.isEditable) return false;
6289
+ if (!isNodeInSchema("table", editor)) return false;
6290
+ if (hideWhenUnavailable) {
6291
+ if (isNodeSelection4(editor.state.selection) || !canToggle) {
6292
+ return false;
6293
+ }
6294
+ }
6295
+ return true;
6296
+ }
6297
+ function useTableBlock(config) {
6298
+ const {
6299
+ editor: providedEditor,
6300
+ hideWhenUnavailable = false,
6301
+ onToggled
6302
+ } = config || {};
6303
+ const intl = useIntl12();
6304
+ const { editor } = useTiptapEditor9(providedEditor);
6305
+ const [isVisible, setIsVisible] = useState20(true);
6306
+ const canToggleState = canToggle(editor);
6307
+ const isActive = (editor == null ? void 0 : editor.isActive("table")) || false;
6308
+ useEffect21(() => {
6309
+ if (!editor) return;
6310
+ const handleSelectionUpdate = () => {
6311
+ setIsVisible(shouldShowButton2({ editor, hideWhenUnavailable }));
6312
+ };
6693
6313
  handleSelectionUpdate();
6694
6314
  editor.on("selectionUpdate", handleSelectionUpdate);
6695
6315
  return () => {
6696
6316
  editor.off("selectionUpdate", handleSelectionUpdate);
6697
6317
  };
6698
6318
  }, [editor, hideWhenUnavailable]);
6699
- const handleColorHighlight = React14.useCallback(() => {
6700
- if (!editor || !canColorHighlightState || !highlightColor || !label)
6701
- return false;
6702
- if (editor.state.storedMarks) {
6703
- const highlightMarkType = editor.schema.marks.highlight;
6704
- if (highlightMarkType) {
6705
- editor.view.dispatch(
6706
- editor.state.tr.removeStoredMark(highlightMarkType)
6707
- );
6319
+ const handleToggle = useCallback21(() => {
6320
+ if (!editor) return false;
6321
+ const success = toggleTable(editor);
6322
+ if (success) {
6323
+ onToggled == null ? void 0 : onToggled();
6324
+ }
6325
+ return success;
6326
+ }, [editor, onToggled]);
6327
+ return {
6328
+ isVisible,
6329
+ isActive,
6330
+ handleToggle,
6331
+ canToggle: canToggleState,
6332
+ label: intl.formatMessage(messages7.table_insert),
6333
+ // shortcutKeys: CODE_BLOCK_SHORTCUT_KEY,
6334
+ Icon: TableIcon
6335
+ };
6336
+ }
6337
+
6338
+ // src/ui/slash-dropdown-menu/use-slash-dropdown-menu.ts
6339
+ function createSlashMenuTexts(formatMessage) {
6340
+ return {
6341
+ // AI
6342
+ continue_writing: {
6343
+ title: formatMessage(messages7.slash_continue_writing),
6344
+ subtext: formatMessage(messages7.slash_continue_writing_subtext),
6345
+ keywords: ["continue", "write", "continue writing", "ai"],
6346
+ badge: AiSparklesIcon,
6347
+ group: formatMessage(messages7.group_ai)
6348
+ },
6349
+ ai_ask_button: {
6350
+ title: formatMessage(messages7.slash_ask_ai),
6351
+ subtext: formatMessage(messages7.slash_ask_ai_subtext),
6352
+ keywords: ["ai", "ask", "generate"],
6353
+ badge: AiSparklesIcon,
6354
+ group: formatMessage(messages7.group_ai)
6355
+ },
6356
+ // Style
6357
+ text: {
6358
+ title: formatMessage(messages7.slash_text),
6359
+ subtext: formatMessage(messages7.slash_text_subtext),
6360
+ keywords: ["p", "paragraph", "text"],
6361
+ badge: TypeIcon,
6362
+ group: formatMessage(messages7.group_style)
6363
+ },
6364
+ heading_1: {
6365
+ title: formatMessage(messages7.slash_heading_1),
6366
+ subtext: formatMessage(messages7.slash_heading_1_subtext),
6367
+ keywords: ["h", "heading1", "h1"],
6368
+ badge: HeadingOneIcon,
6369
+ group: formatMessage(messages7.group_style)
6370
+ },
6371
+ heading_2: {
6372
+ title: formatMessage(messages7.slash_heading_2),
6373
+ subtext: formatMessage(messages7.slash_heading_2_subtext),
6374
+ keywords: ["h2", "heading2", "subheading"],
6375
+ badge: HeadingTwoIcon,
6376
+ group: formatMessage(messages7.group_style)
6377
+ },
6378
+ heading_3: {
6379
+ title: formatMessage(messages7.slash_heading_3),
6380
+ subtext: formatMessage(messages7.slash_heading_3_subtext),
6381
+ keywords: ["h3", "heading3", "subheading"],
6382
+ badge: HeadingThreeIcon,
6383
+ group: formatMessage(messages7.group_style)
6384
+ },
6385
+ bullet_list: {
6386
+ title: formatMessage(messages7.slash_bullet_list),
6387
+ subtext: formatMessage(messages7.slash_bullet_list_subtext),
6388
+ keywords: ["ul", "li", "list", "bulletlist", "bullet list"],
6389
+ badge: ListIcon2,
6390
+ group: formatMessage(messages7.group_style)
6391
+ },
6392
+ ordered_list: {
6393
+ title: formatMessage(messages7.slash_ordered_list),
6394
+ subtext: formatMessage(messages7.slash_ordered_list_subtext),
6395
+ keywords: ["ol", "li", "list", "numberedlist", "numbered list"],
6396
+ badge: ListOrderedIcon,
6397
+ group: formatMessage(messages7.group_style)
6398
+ },
6399
+ task_list: {
6400
+ title: formatMessage(messages7.slash_task_list),
6401
+ subtext: formatMessage(messages7.slash_task_list_subtext),
6402
+ keywords: ["tasklist", "task list", "todo", "checklist"],
6403
+ badge: ListTodoIcon,
6404
+ group: formatMessage(messages7.group_style)
6405
+ },
6406
+ quote: {
6407
+ title: formatMessage(messages7.slash_blockquote),
6408
+ subtext: formatMessage(messages7.slash_blockquote_subtext),
6409
+ keywords: ["quote", "blockquote"],
6410
+ badge: BlockquoteIcon,
6411
+ group: formatMessage(messages7.group_style)
6412
+ },
6413
+ code_block: {
6414
+ title: formatMessage(messages7.slash_code_block),
6415
+ subtext: formatMessage(messages7.slash_code_block_subtext),
6416
+ keywords: ["code", "pre"],
6417
+ badge: CodeBlockIcon,
6418
+ group: formatMessage(messages7.group_style)
6419
+ },
6420
+ // Insert
6421
+ control: {
6422
+ title: formatMessage(messages7.slash_control),
6423
+ subtext: formatMessage(messages7.slash_control_subtext),
6424
+ keywords: ["control"],
6425
+ badge: ControlsIcon,
6426
+ group: formatMessage(messages7.group_insert)
6427
+ },
6428
+ divider: {
6429
+ title: formatMessage(messages7.slash_separator),
6430
+ subtext: formatMessage(messages7.slash_separator_subtext),
6431
+ keywords: ["hr", "horizontalRule", "line", "separator"],
6432
+ badge: MinusIcon,
6433
+ group: formatMessage(messages7.group_insert)
6434
+ },
6435
+ table: {
6436
+ title: formatMessage(messages7.slash_table),
6437
+ subtext: formatMessage(messages7.slash_table_subtext),
6438
+ keywords: ["table", "grid", "spreadsheet"],
6439
+ badge: TableIcon2,
6440
+ group: formatMessage(messages7.group_insert)
6441
+ },
6442
+ table_of_contents: {
6443
+ title: formatMessage(messages7.slash_toc),
6444
+ subtext: formatMessage(messages7.slash_toc_subtext),
6445
+ keywords: ["toc", "table of contents", "index", "navigation", "headings"],
6446
+ badge: TableOfContentsIcon,
6447
+ group: formatMessage(messages7.group_insert)
6448
+ },
6449
+ callout: {
6450
+ title: formatMessage(messages7.slash_callout),
6451
+ subtext: formatMessage(messages7.slash_callout_subtext),
6452
+ keywords: ["callout", "info", "warning", "alert", "note", "tip"],
6453
+ badge: InfoIcon2,
6454
+ group: formatMessage(messages7.group_insert)
6455
+ },
6456
+ callout_warning: {
6457
+ title: formatMessage(messages7.slash_warning),
6458
+ subtext: formatMessage(messages7.slash_warning_subtext),
6459
+ keywords: ["warning", "caution", "attention"],
6460
+ badge: AlertIcon2,
6461
+ group: formatMessage(messages7.group_insert)
6462
+ },
6463
+ math: {
6464
+ title: formatMessage(messages7.slash_formula),
6465
+ subtext: formatMessage(messages7.slash_formula_subtext),
6466
+ keywords: ["math", "latex", "formula", "equation", "katex"],
6467
+ badge: TypeIcon,
6468
+ group: formatMessage(messages7.group_insert)
6469
+ },
6470
+ // Upload
6471
+ image: {
6472
+ title: formatMessage(messages7.slash_image),
6473
+ subtext: formatMessage(messages7.slash_image_subtext),
6474
+ keywords: [
6475
+ "image",
6476
+ "imageUpload",
6477
+ "upload",
6478
+ "img",
6479
+ "picture",
6480
+ "media",
6481
+ "url"
6482
+ ],
6483
+ badge: ImageIcon2,
6484
+ group: formatMessage(messages7.group_upload)
6485
+ }
6486
+ };
6487
+ }
6488
+ var getItemImplementations = () => {
6489
+ return {
6490
+ // AI
6491
+ continue_writing: {
6492
+ check: (editor) => {
6493
+ const { hasContent } = hasContentAbove(editor);
6494
+ const extensionsReady = isExtensionAvailable(editor, [
6495
+ "ai",
6496
+ "aiAdvanced"
6497
+ ]);
6498
+ return extensionsReady && hasContent;
6499
+ },
6500
+ action: ({ editor }) => {
6501
+ const editorChain = editor.chain().focus();
6502
+ const nodeSelectionPosition = findSelectionPosition({ editor });
6503
+ if (nodeSelectionPosition !== null) {
6504
+ editorChain.setNodeSelection(nodeSelectionPosition);
6505
+ }
6506
+ editorChain.run();
6507
+ editor.chain().focus().aiGenerationShow().run();
6508
+ requestAnimationFrame(() => {
6509
+ const { hasContent, content } = hasContentAbove(editor);
6510
+ const snippet = content.length > 500 ? `...${content.slice(-500)}` : content;
6511
+ const prompt = hasContent ? `Context: ${snippet}
6512
+
6513
+ Continue writing from where the text above ends. Write ONLY ONE SENTENCE. DONT REPEAT THE TEXT.` : "Start writing a new paragraph. Write ONLY ONE SENTENCE.";
6514
+ editor.chain().focus().aiTextPrompt({
6515
+ stream: true,
6516
+ format: "rich-text",
6517
+ text: prompt
6518
+ }).run();
6519
+ });
6520
+ }
6521
+ },
6522
+ ai_ask_button: {
6523
+ check: (editor) => isExtensionAvailable(editor, ["ai", "aiAdvanced"]),
6524
+ action: ({ editor }) => {
6525
+ const editorChain = editor.chain().focus();
6526
+ const nodeSelectionPosition = findSelectionPosition({ editor });
6527
+ if (nodeSelectionPosition !== null) {
6528
+ editorChain.setNodeSelection(nodeSelectionPosition);
6529
+ }
6530
+ editorChain.run();
6531
+ editor.chain().focus().aiGenerationShow().run();
6532
+ }
6533
+ },
6534
+ // Style
6535
+ text: {
6536
+ check: (editor) => isNodeInSchema2("paragraph", editor),
6537
+ action: ({ editor }) => {
6538
+ editor.chain().focus().setParagraph().run();
6539
+ }
6540
+ },
6541
+ heading_1: {
6542
+ check: (editor) => isNodeInSchema2("heading", editor),
6543
+ action: ({ editor }) => {
6544
+ editor.chain().focus().toggleHeading({ level: 1 }).run();
6545
+ }
6546
+ },
6547
+ heading_2: {
6548
+ check: (editor) => isNodeInSchema2("heading", editor),
6549
+ action: ({ editor }) => {
6550
+ editor.chain().focus().toggleHeading({ level: 2 }).run();
6551
+ }
6552
+ },
6553
+ heading_3: {
6554
+ check: (editor) => isNodeInSchema2("heading", editor),
6555
+ action: ({ editor }) => {
6556
+ editor.chain().focus().toggleHeading({ level: 3 }).run();
6557
+ }
6558
+ },
6559
+ bullet_list: {
6560
+ check: (editor) => isNodeInSchema2("bulletList", editor),
6561
+ action: ({ editor }) => {
6562
+ editor.chain().focus().toggleBulletList().run();
6563
+ }
6564
+ },
6565
+ ordered_list: {
6566
+ check: (editor) => isNodeInSchema2("orderedList", editor),
6567
+ action: ({ editor }) => {
6568
+ editor.chain().focus().toggleOrderedList().run();
6569
+ }
6570
+ },
6571
+ task_list: {
6572
+ check: (editor) => isNodeInSchema2("taskList", editor),
6573
+ action: ({ editor }) => {
6574
+ editor.chain().focus().toggleTaskList().run();
6575
+ }
6576
+ },
6577
+ quote: {
6578
+ check: (editor) => isNodeInSchema2("blockquote", editor),
6579
+ action: ({ editor }) => {
6580
+ editor.chain().focus().toggleBlockquote().run();
6581
+ }
6582
+ },
6583
+ code_block: {
6584
+ check: (editor) => isNodeInSchema2("codeBlock", editor),
6585
+ action: ({ editor }) => {
6586
+ editor.chain().focus().toggleNode("codeBlock", "paragraph").run();
6587
+ }
6588
+ },
6589
+ // Insert
6590
+ // mention: {
6591
+ // check: (editor: Editor) =>
6592
+ // isExtensionAvailable(editor, ["mention", "mentionAdvanced"]),
6593
+ // action: ({ editor }: { editor: Editor }) => addMentionTrigger(editor),
6594
+ // },
6595
+ // emoji: {
6596
+ // check: (editor: Editor) =>
6597
+ // isExtensionAvailable(editor, ["emoji", "emojiPicker"]),
6598
+ // action: ({ editor }: { editor: Editor }) => addEmojiTrigger(editor),
6599
+ // },
6600
+ divider: {
6601
+ check: (editor) => isNodeInSchema2("horizontalRule", editor),
6602
+ action: ({ editor }) => {
6603
+ editor.chain().focus().setHorizontalRule().run();
6604
+ }
6605
+ },
6606
+ table: {
6607
+ check: (editor) => isNodeInSchema2("table", editor),
6608
+ action: ({ editor }) => toggleTable(editor, { rows: 3, cols: 3, withHeaderRow: true })
6609
+ },
6610
+ control: {
6611
+ check: (editor) => isNodeInSchema2("controlBlock", editor),
6612
+ action: ({ editor }) => {
6613
+ try {
6614
+ return editor.chain().focus().insertControlBlock().run();
6615
+ } catch (e) {
6616
+ console.error(e);
6617
+ }
6618
+ }
6619
+ },
6620
+ table_of_contents: {
6621
+ check: (editor) => isNodeInSchema2("tableOfContentsNode", editor),
6622
+ action: ({ editor }) => {
6623
+ editor.chain().focus().insertTableOfContents().run();
6624
+ }
6625
+ },
6626
+ callout: {
6627
+ check: (editor) => isNodeInSchema2("calloutNode", editor),
6628
+ action: ({ editor }) => {
6629
+ editor.chain().focus().insertCallout("info").run();
6630
+ }
6631
+ },
6632
+ callout_warning: {
6633
+ check: (editor) => isNodeInSchema2("calloutNode", editor),
6634
+ action: ({ editor }) => {
6635
+ editor.chain().focus().insertCallout("warning").run();
6636
+ }
6637
+ },
6638
+ math: {
6639
+ check: (editor) => isNodeInSchema2("mathBlock", editor),
6640
+ action: ({ editor }) => {
6641
+ editor.chain().focus().insertMathBlock().run();
6642
+ }
6643
+ },
6644
+ // Upload
6645
+ image: {
6646
+ check: (editor) => isNodeInSchema2("imageUpload", editor),
6647
+ action: ({ editor }) => {
6648
+ editor.chain().focus().setImageUpload().run();
6708
6649
  }
6709
6650
  }
6710
- setTimeout(() => {
6711
- const success = editor.chain().focus().toggleMark("highlight", { color: highlightColor }).run();
6712
- if (success) {
6713
- onApplied == null ? void 0 : onApplied({ color: highlightColor, label });
6714
- }
6715
- return success;
6716
- }, 0);
6717
- }, [canColorHighlightState, highlightColor, editor, label, onApplied]);
6718
- const handleRemoveHighlight = React14.useCallback(() => {
6719
- const success = removeHighlight(editor);
6720
- if (success) {
6721
- onApplied == null ? void 0 : onApplied({ color: "", label: "Remove highlight" });
6651
+ };
6652
+ };
6653
+ function organizeItemsByGroups(items, showGroups) {
6654
+ if (!showGroups) {
6655
+ return items.map((item) => ({ ...item, group: "" }));
6656
+ }
6657
+ const groups = {};
6658
+ items.forEach((item) => {
6659
+ const groupLabel = item.group || "";
6660
+ if (!groups[groupLabel]) {
6661
+ groups[groupLabel] = [];
6722
6662
  }
6723
- return success;
6724
- }, [editor, onApplied]);
6725
- useHotkeys(
6726
- COLOR_HIGHLIGHT_SHORTCUT_KEY,
6727
- (event) => {
6728
- event.preventDefault();
6729
- handleColorHighlight();
6663
+ groups[groupLabel].push(item);
6664
+ });
6665
+ const organizedItems = [];
6666
+ Object.entries(groups).forEach(([, groupItems]) => {
6667
+ organizedItems.push(...groupItems);
6668
+ });
6669
+ return organizedItems;
6670
+ }
6671
+ var ALL_SLASH_MENU_ITEMS = [
6672
+ "continue_writing",
6673
+ "ai_ask_button",
6674
+ "text",
6675
+ "heading_1",
6676
+ "heading_2",
6677
+ "heading_3",
6678
+ "bullet_list",
6679
+ "ordered_list",
6680
+ "task_list",
6681
+ "quote",
6682
+ "code_block",
6683
+ "control",
6684
+ "divider",
6685
+ "table",
6686
+ "table_of_contents",
6687
+ "callout",
6688
+ "callout_warning",
6689
+ "math",
6690
+ "image"
6691
+ ];
6692
+ function useSlashDropdownMenu(config) {
6693
+ const intl = useIntl13();
6694
+ const getSlashMenuItems = React11.useCallback(
6695
+ (editor) => {
6696
+ const items = [];
6697
+ const texts = createSlashMenuTexts(intl.formatMessage);
6698
+ const enabledItems = (config == null ? void 0 : config.enabledItems) || ALL_SLASH_MENU_ITEMS;
6699
+ const showGroups = (config == null ? void 0 : config.showGroups) !== false;
6700
+ const itemImplementations = getItemImplementations();
6701
+ enabledItems.forEach((itemType) => {
6702
+ var _a;
6703
+ const itemImpl = itemImplementations[itemType];
6704
+ const itemText = texts[itemType];
6705
+ if (itemImpl && itemText && itemImpl.check(editor)) {
6706
+ const item = {
6707
+ onSelect: ({ editor: editor2 }) => itemImpl.action({ editor: editor2 }),
6708
+ ...itemText
6709
+ };
6710
+ if ((_a = config == null ? void 0 : config.itemGroups) == null ? void 0 : _a[itemType]) {
6711
+ item.group = config.itemGroups[itemType];
6712
+ } else if (!showGroups) {
6713
+ item.group = "";
6714
+ }
6715
+ items.push(item);
6716
+ }
6717
+ });
6718
+ if (config == null ? void 0 : config.customItems) {
6719
+ items.push(...config.customItems);
6720
+ }
6721
+ return organizeItemsByGroups(items, showGroups);
6730
6722
  },
6731
- {
6732
- enabled: isVisible && canColorHighlightState,
6733
- enableOnContentEditable: !isMobile,
6734
- enableOnFormTags: true
6735
- }
6723
+ [config, intl]
6736
6724
  );
6737
6725
  return {
6738
- isVisible,
6739
- isActive,
6740
- handleColorHighlight,
6741
- handleRemoveHighlight,
6742
- canColorHighlight: canColorHighlightState,
6743
- label: label || intl.formatMessage(messages7.highlight_color),
6744
- shortcutKeys: COLOR_HIGHLIGHT_SHORTCUT_KEY,
6745
- Icon: HighlighterIcon
6726
+ getSlashMenuItems,
6727
+ config
6746
6728
  };
6747
6729
  }
6748
6730
 
6749
- // src/ui/color-highlight-button/color-highlight-button.tsx
6750
- import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
6751
- var ColorHighlightButton = ({
6752
- editor: providedEditor,
6753
- highlightColor,
6754
- text,
6755
- hideWhenUnavailable = false,
6756
- onApplied,
6757
- showShortcut = false,
6758
- onClick,
6759
- children,
6760
- style,
6761
- className,
6762
- ...buttonProps
6763
- }) => {
6764
- const { editor } = useTiptapEditor8(providedEditor);
6765
- const {
6766
- isVisible,
6767
- canColorHighlight: canColorHighlight2,
6768
- isActive,
6769
- handleColorHighlight,
6770
- label,
6771
- shortcutKeys
6772
- } = useColorHighlight({
6773
- editor,
6774
- highlightColor,
6775
- label: text || `Toggle highlight (${highlightColor})`,
6776
- hideWhenUnavailable,
6777
- onApplied
6778
- });
6779
- const handleClick = useCallback23(
6780
- (event) => {
6781
- onClick == null ? void 0 : onClick(event);
6782
- if (event.defaultPrevented) return;
6783
- handleColorHighlight();
6784
- },
6785
- [handleColorHighlight, onClick]
6731
+ // src/ui/slash-dropdown-menu/slash-dropdown-menu.tsx
6732
+ import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
6733
+ var SlashDropdownMenu = (props) => {
6734
+ const { config, ...restProps } = props;
6735
+ const { getSlashMenuItems } = useSlashDropdownMenu(config);
6736
+ return /* @__PURE__ */ jsx22(
6737
+ SuggestionMenu,
6738
+ {
6739
+ char: "/",
6740
+ pluginKey: "slashDropdownMenu",
6741
+ decorationClass: "tiptap-slash-decoration",
6742
+ decorationContent: "Filter...",
6743
+ selector: "tiptap-slash-dropdown-menu",
6744
+ items: ({ query, editor }) => filterSuggestionItems(getSlashMenuItems(editor), query),
6745
+ ...restProps,
6746
+ children: (props2) => /* @__PURE__ */ jsx22(List, { ...props2, config })
6747
+ }
6786
6748
  );
6787
- const buttonStyle = useMemo15(
6788
- () => ({
6789
- ...style,
6790
- "--highlight-color": highlightColor
6791
- }),
6792
- [highlightColor, style]
6749
+ };
6750
+ var Item = (props) => {
6751
+ const { item, isSelected, onSelect } = props;
6752
+ const itemRef = React12.useRef(null);
6753
+ React12.useEffect(() => {
6754
+ const selector = document.querySelector(
6755
+ '[data-selector="tiptap-slash-dropdown-menu"]'
6756
+ );
6757
+ if (!itemRef.current || !isSelected || !selector) return;
6758
+ const overflow = getElementOverflowPosition2(itemRef.current, selector);
6759
+ if (overflow === "top") {
6760
+ itemRef.current.scrollIntoView(true);
6761
+ } else if (overflow === "bottom") {
6762
+ itemRef.current.scrollIntoView(false);
6763
+ }
6764
+ }, [isSelected]);
6765
+ const BadgeIcon = item.badge;
6766
+ return /* @__PURE__ */ jsx22(
6767
+ Button6,
6768
+ {
6769
+ ref: itemRef,
6770
+ variant: "ghost",
6771
+ color: "default",
6772
+ startContent: BadgeIcon && /* @__PURE__ */ jsx22(BadgeIcon, {}),
6773
+ "data-active-state": isSelected ? "on" : "off",
6774
+ onClick: onSelect,
6775
+ fullWidth: true,
6776
+ spacing: "start",
6777
+ children: item.title
6778
+ }
6793
6779
  );
6794
- if (!isVisible) {
6780
+ };
6781
+ var List = ({
6782
+ items,
6783
+ selectedIndex,
6784
+ onSelect,
6785
+ config
6786
+ }) => {
6787
+ const styles = slashDropdownMenu2();
6788
+ const renderedItems = React12.useMemo(() => {
6789
+ const rendered = [];
6790
+ const showGroups = (config == null ? void 0 : config.showGroups) !== false;
6791
+ if (!showGroups) {
6792
+ items.forEach((item, index) => {
6793
+ rendered.push(
6794
+ /* @__PURE__ */ jsx22(
6795
+ Item,
6796
+ {
6797
+ item,
6798
+ isSelected: index === selectedIndex,
6799
+ onSelect: () => onSelect(item)
6800
+ },
6801
+ `item-${index}-${item.title}`
6802
+ )
6803
+ );
6804
+ });
6805
+ return rendered;
6806
+ }
6807
+ const groups = {};
6808
+ items.forEach((item, index) => {
6809
+ const groupLabel = item.group || "";
6810
+ if (!groups[groupLabel]) {
6811
+ groups[groupLabel] = { items: [], indices: [] };
6812
+ }
6813
+ groups[groupLabel].items.push(item);
6814
+ groups[groupLabel].indices.push(index);
6815
+ });
6816
+ Object.entries(groups).forEach(([groupLabel, groupData], groupIndex) => {
6817
+ if (groupIndex > 0) {
6818
+ rendered.push(
6819
+ /* @__PURE__ */ jsx22(
6820
+ Separator,
6821
+ {
6822
+ orientation: "horizontal"
6823
+ },
6824
+ `separator-${groupIndex.toString()}`
6825
+ )
6826
+ );
6827
+ }
6828
+ const groupItems = groupData.items.map((item, itemIndex) => {
6829
+ const originalIndex = groupData.indices[itemIndex];
6830
+ return /* @__PURE__ */ jsx22(
6831
+ Item,
6832
+ {
6833
+ item,
6834
+ isSelected: originalIndex === selectedIndex,
6835
+ onSelect: () => onSelect(item)
6836
+ },
6837
+ `item-${originalIndex}-${item.title}`
6838
+ );
6839
+ });
6840
+ if (groupLabel) {
6841
+ rendered.push(
6842
+ /* @__PURE__ */ jsxs17(
6843
+ "div",
6844
+ {
6845
+ className: styles.cardItemGroup(),
6846
+ children: [
6847
+ /* @__PURE__ */ jsx22("div", { className: styles.cardGroupLabel(), children: groupLabel }),
6848
+ /* @__PURE__ */ jsx22("div", { className: styles.cardGroup(), children: groupItems })
6849
+ ]
6850
+ },
6851
+ `group-${groupIndex}-${groupLabel}`
6852
+ )
6853
+ );
6854
+ } else {
6855
+ rendered.push(...groupItems);
6856
+ }
6857
+ });
6858
+ return rendered;
6859
+ }, [items, selectedIndex, onSelect, config == null ? void 0 : config.showGroups, styles]);
6860
+ if (!renderedItems.length) {
6795
6861
  return null;
6796
6862
  }
6797
- const styles = colorHighlightButton();
6798
- return /* @__PURE__ */ jsxs16(
6799
- ToolbarButton3,
6863
+ return /* @__PURE__ */ jsx22(
6864
+ "div",
6800
6865
  {
6801
- type: "button",
6802
- disabled: !canColorHighlight2,
6803
- "data-disabled": !canColorHighlight2,
6804
- variant: "ghost",
6805
- color: "default",
6806
- "data-active-state": isActive ? "on" : "off",
6807
- tabIndex: -1,
6808
- "aria-label": label,
6809
- shortcutKeys,
6810
- "aria-pressed": isActive,
6811
- onClick: handleClick,
6812
- style: buttonStyle,
6813
- className: styles.button({ className }),
6814
- isIconOnly: true,
6815
- ...buttonProps,
6816
- children: [
6817
- /* @__PURE__ */ jsx21(
6818
- "span",
6819
- {
6820
- "data-active-state": isActive ? "on" : "off",
6821
- className: styles.mark()
6822
- }
6823
- ),
6824
- children || /* @__PURE__ */ jsxs16(Fragment4, { children: [
6825
- /* @__PURE__ */ jsx21(
6826
- "span",
6827
- {
6828
- style: { "--highlight-color": highlightColor }
6829
- }
6830
- ),
6831
- text
6832
- ] })
6833
- ]
6866
+ className: styles.card(),
6867
+ style: {
6868
+ maxHeight: "var(--suggestion-menu-max-height)"
6869
+ },
6870
+ children: /* @__PURE__ */ jsx22("div", { className: styles.body(), children: renderedItems })
6834
6871
  }
6835
6872
  );
6836
6873
  };
6837
6874
 
6838
- // src/ui/color-highlight-popover/color-highlight-popover.tsx
6839
- import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
6840
- var ColorHighlightPopoverButton = ({
6841
- className,
6842
- children,
6843
- ...props
6844
- }) => /* @__PURE__ */ jsx22(
6845
- IconButton8,
6846
- {
6847
- type: "button",
6848
- className,
6849
- variant: "ghost",
6850
- color: "default",
6851
- tabIndex: -1,
6852
- "aria-label": "Highlight text",
6853
- tooltip: "Highlight",
6854
- isIconOnly: !children,
6855
- ...props,
6856
- children: children != null ? children : /* @__PURE__ */ jsx22(HighlighterIcon2, {})
6857
- }
6858
- );
6859
- function ColorHighlightPopoverContent({
6860
- editor,
6861
- colors = pickHighlightColorsByValue([
6862
- "var(--tt-color-highlight-green)",
6863
- "var(--tt-color-highlight-blue)",
6864
- "var(--tt-color-highlight-red)",
6865
- "var(--tt-color-highlight-purple)",
6866
- "var(--tt-color-highlight-yellow)"
6867
- ])
6868
- }) {
6869
- const { handleRemoveHighlight } = useColorHighlight({ editor });
6870
- const containerRef = useRef11(null);
6871
- const menuItems = useMemo16(
6872
- () => [...colors, { label: "Remove highlight", value: "none" }],
6873
- [colors]
6874
- );
6875
- const { selectedIndex } = useMenuNavigation({
6876
- containerRef,
6877
- items: menuItems,
6878
- orientation: "both",
6879
- onSelect: (item) => {
6880
- if (!containerRef.current) return false;
6881
- const highlightedElement = containerRef.current.querySelector(
6882
- '[data-highlighted="true"]'
6883
- );
6884
- if (highlightedElement) highlightedElement.click();
6885
- if (item.value === "none") handleRemoveHighlight();
6886
- },
6887
- autoSelectFirstItem: false
6875
+ // src/presets/basic/editor-header.tsx
6876
+ import { useTiptapEditor as useTiptapEditor26 } from "@kopexa/editor-utils";
6877
+ import { MoreVerticalIcon } from "@kopexa/icons";
6878
+ import { Popover as Popover3 } from "@kopexa/popover";
6879
+ import {
6880
+ Toolbar as Toolbar2,
6881
+ ToolbarButton as ToolbarButton8,
6882
+ ToolbarGroup as ToolbarGroup2,
6883
+ ToolbarSeparator as ToolbarSeparator3
6884
+ } from "@kopexa/toolbar";
6885
+ import { useIsMobile as useIsMobile3 } from "@kopexa/use-is-mobile";
6886
+ import { useEffect as useEffect31, useRef as useRef12, useState as useState30 } from "react";
6887
+ import { useIntl as useIntl18 } from "react-intl";
6888
+
6889
+ // src/hooks/use-cursor-visibility.ts
6890
+ import * as React14 from "react";
6891
+
6892
+ // src/hooks/use-window-size.ts
6893
+ import * as React13 from "react";
6894
+ function useWindowSize() {
6895
+ const [windowSize, setWindowSize] = React13.useState({
6896
+ width: 0,
6897
+ height: 0,
6898
+ offsetTop: 0
6888
6899
  });
6889
- return /* @__PURE__ */ jsxs17("div", { ref: containerRef, className: "flex gap-1 items-center", children: [
6890
- /* @__PURE__ */ jsx22(
6891
- "div",
6892
- {
6893
- className: "flex items-center gap-1 outline-none",
6894
- "data-orientation": "horizontal",
6895
- children: colors.map((color, index) => /* @__PURE__ */ jsx22(
6896
- ColorHighlightButton,
6897
- {
6898
- editor,
6899
- highlightColor: color.value,
6900
- "aria-label": `${color.label} highlight color`,
6901
- tabIndex: index === selectedIndex ? 0 : -1,
6902
- "data-highlighted": selectedIndex === index
6903
- },
6904
- color.value
6905
- ))
6906
- }
6907
- ),
6908
- /* @__PURE__ */ jsx22(ToolbarSeparator2, { orientation: "vertical" }),
6909
- /* @__PURE__ */ jsx22("div", { className: "tiptap-button-group", children: /* @__PURE__ */ jsx22(
6910
- IconButton8,
6911
- {
6912
- onClick: handleRemoveHighlight,
6913
- "aria-label": "Remove highlight",
6914
- tabIndex: selectedIndex === colors.length ? 0 : -1,
6915
- type: "button",
6916
- role: "menuitem",
6917
- variant: "ghost",
6918
- color: "default",
6919
- "data-highlighted": selectedIndex === colors.length,
6920
- children: /* @__PURE__ */ jsx22(BanIcon, {})
6900
+ React13.useEffect(() => {
6901
+ handleResize();
6902
+ function handleResize() {
6903
+ if (typeof window === "undefined") return;
6904
+ const vp = window.visualViewport;
6905
+ if (!vp) return;
6906
+ const { width = 0, height = 0, offsetTop = 0 } = vp;
6907
+ setWindowSize((state) => {
6908
+ if (width === state.width && height === state.height && offsetTop === state.offsetTop) {
6909
+ return state;
6910
+ }
6911
+ return { width, height, offsetTop };
6912
+ });
6913
+ }
6914
+ const visualViewport = window.visualViewport;
6915
+ if (visualViewport) {
6916
+ visualViewport.addEventListener("resize", handleResize);
6917
+ visualViewport.addEventListener("scroll", handleResize);
6918
+ }
6919
+ return () => {
6920
+ if (visualViewport) {
6921
+ visualViewport.removeEventListener("resize", handleResize);
6922
+ visualViewport.removeEventListener("scroll", handleResize);
6921
6923
  }
6922
- ) })
6923
- ] });
6924
+ };
6925
+ }, []);
6926
+ return windowSize;
6924
6927
  }
6925
- function ColorHighlightPopover({
6926
- editor: providedEditor,
6927
- colors = pickHighlightColorsByValue([
6928
- "var(--tt-color-highlight-green)",
6929
- "var(--tt-color-highlight-blue)",
6930
- "var(--tt-color-highlight-red)",
6931
- "var(--tt-color-highlight-purple)",
6932
- "var(--tt-color-highlight-yellow)"
6933
- ]),
6934
- hideWhenUnavailable = false,
6935
- onApplied,
6936
- ...props
6928
+
6929
+ // src/hooks/use-cursor-visibility.ts
6930
+ function useCursorVisibility({
6931
+ editor,
6932
+ overlayHeight = 0,
6933
+ elementRef = null
6937
6934
  }) {
6938
- const { editor } = useTiptapEditor9(providedEditor);
6939
- const [isOpen, setIsOpen] = useState22(false);
6940
- const { isVisible, canColorHighlight: canColorHighlight2, isActive, label } = useColorHighlight({
6941
- editor,
6942
- hideWhenUnavailable,
6943
- onApplied
6935
+ const { height: windowHeight } = useWindowSize();
6936
+ const [rect, setRect] = React14.useState({
6937
+ x: 0,
6938
+ y: 0,
6939
+ width: 0,
6940
+ height: 0
6944
6941
  });
6945
- if (!isVisible) return null;
6946
- return /* @__PURE__ */ jsxs17(Popover2.Root, { open: isOpen, onOpenChange: setIsOpen, spacing: "dense", children: [
6947
- /* @__PURE__ */ jsx22(
6948
- Popover2.Trigger,
6949
- {
6950
- render: /* @__PURE__ */ jsx22(
6951
- ColorHighlightPopoverButton,
6952
- {
6953
- disabled: !canColorHighlight2,
6954
- "data-disabled": !canColorHighlight2,
6955
- "data-active-state": isActive ? "on" : "off",
6956
- "aria-pressed": isActive,
6957
- "aria-label": label,
6958
- title: label,
6959
- ...props
6942
+ const updateRect = React14.useCallback(() => {
6943
+ var _a;
6944
+ const element = (_a = elementRef == null ? void 0 : elementRef.current) != null ? _a : document.body;
6945
+ const { x, y, width, height } = element.getBoundingClientRect();
6946
+ setRect({ x, y, width, height });
6947
+ }, [elementRef]);
6948
+ React14.useEffect(() => {
6949
+ var _a;
6950
+ const element = (_a = elementRef == null ? void 0 : elementRef.current) != null ? _a : document.body;
6951
+ updateRect();
6952
+ const resizeObserver = new ResizeObserver(() => {
6953
+ window.requestAnimationFrame(updateRect);
6954
+ });
6955
+ resizeObserver.observe(element);
6956
+ window.addEventListener("scroll", updateRect, { passive: true });
6957
+ return () => {
6958
+ resizeObserver.disconnect();
6959
+ window.removeEventListener("scroll", updateRect);
6960
+ };
6961
+ }, [elementRef, updateRect]);
6962
+ React14.useEffect(() => {
6963
+ const ensureCursorVisibility = () => {
6964
+ if (!editor) return;
6965
+ const { state, view } = editor;
6966
+ if (!view.hasFocus()) return;
6967
+ const { from } = state.selection;
6968
+ const cursorCoords = view.coordsAtPos(from);
6969
+ if (windowHeight < rect.height) {
6970
+ if (cursorCoords) {
6971
+ const availableSpace = windowHeight - cursorCoords.top - overlayHeight > 0;
6972
+ if (!availableSpace) {
6973
+ const targetScrollY = (
6974
+ // TODO: Needed?
6975
+ // window.scrollY + (cursorCoords.top - windowHeight / 2)
6976
+ cursorCoords.top - windowHeight / 2
6977
+ );
6978
+ window.scrollTo({
6979
+ top: targetScrollY,
6980
+ behavior: "smooth"
6981
+ });
6960
6982
  }
6961
- )
6983
+ }
6962
6984
  }
6963
- ),
6964
- /* @__PURE__ */ jsx22(Popover2.Content, { "aria-label": "Highlight colors", children: /* @__PURE__ */ jsx22(ColorHighlightPopoverContent, { editor, colors }) })
6965
- ] });
6985
+ };
6986
+ ensureCursorVisibility();
6987
+ }, [editor, overlayHeight, windowHeight, rect.height]);
6988
+ return rect;
6966
6989
  }
6967
6990
 
6968
6991
  // src/ui/list-dropdown-menu/index.tsx