+ e.preventDefault()}
+ target={isInternal ? undefined : '_blank'}
+ rel={isInternal ? undefined : 'noopener noreferrer'}
+ >
+
+
+
+ {/* Hover Toolbar */}
+ {isEditable && !isTouch && isHovered && !showEditPanel && (
+
+
+
+
+ {isInternal ? (
+
+ ) : (
+
+ )}
+
+ {linkLabel}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Edit Panel */}
+ {isEditable && showEditPanel && (
+ <>
+ {createPortal(
+ ,
+ document.body
+ )}
+ e.stopPropagation()}
+ onMouseDown={(e) => e.stopPropagation()}
+ onMouseUp={(e) => e.stopPropagation()}
+ >
+
+
+ setEditUrl(e.target.value)}
+ onKeyDown={handleKeyDown}
+ rightSection={
+ editUrl && (
+ setEditUrl('')} />
+ )
+ }
+ autoFocus
+ withAsterisk
+ />
+
+ setEditTitle(e.target.value)}
+ onKeyDown={handleKeyDown}
+ />
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ >
+ );
+}
diff --git a/apps/client/src/features/editor/components/link/link.module.css b/apps/client/src/features/editor/components/link/link.module.css
index 2168997f..528cbc3d 100644
--- a/apps/client/src/features/editor/components/link/link.module.css
+++ b/apps/client/src/features/editor/components/link/link.module.css
@@ -1,6 +1,51 @@
.link {
- color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-dark-1));
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
\ No newline at end of file
+ color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-dark-1));
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.linkWrapper {
+ position: relative;
+ display: inline;
+}
+
+.linkText {
+ color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4));
+ text-decoration: underline;
+ text-decoration-color: light-dark(
+ var(--mantine-color-blue-3),
+ var(--mantine-color-blue-7)
+ );
+ text-underline-offset: 2px;
+ cursor: pointer;
+
+ &:hover {
+ text-decoration-color: light-dark(
+ var(--mantine-color-blue-6),
+ var(--mantine-color-blue-4)
+ );
+ }
+}
+
+.linkToolbar {
+ position: absolute;
+ bottom: calc(100% + 6px);
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 100;
+}
+
+.editPanel {
+ position: absolute;
+ top: calc(100% + 6px);
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 101;
+}
+
+.editPanelOverlay {
+ position: fixed;
+ inset: 0;
+ z-index: 100;
+}
diff --git a/apps/client/src/features/editor/components/link/types.ts b/apps/client/src/features/editor/components/link/types.ts
index 853dcae6..a2cc2024 100644
--- a/apps/client/src/features/editor/components/link/types.ts
+++ b/apps/client/src/features/editor/components/link/types.ts
@@ -1,4 +1,5 @@
export type LinkEditorPanelProps = {
initialUrl?: string;
onSetLink: (url: string, openInNewTab?: boolean) => void;
+ onUnsetLink?: () => void;
};
diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts
index ef03108b..380ad7c5 100644
--- a/apps/client/src/features/editor/extensions/extensions.ts
+++ b/apps/client/src/features/editor/extensions/extensions.ts
@@ -72,8 +72,9 @@ import fortran from "highlight.js/lib/languages/fortran";
import haskell from "highlight.js/lib/languages/haskell";
import scala from "highlight.js/lib/languages/scala";
import mentionRenderItems from "@/features/editor/components/mention/mention-suggestion.ts";
-import { ReactNodeViewRenderer } from "@tiptap/react";
+import { ReactNodeViewRenderer, ReactMarkViewRenderer } from "@tiptap/react";
import MentionView from "@/features/editor/components/mention/mention-view.tsx";
+import LinkView from "@/features/editor/components/link/link-view.tsx";
import i18n from "@/i18n.ts";
import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboard.ts";
import EmojiCommand from "./emoji-command";
@@ -136,6 +137,10 @@ export const mainExtensions = [
}),
LinkExtension.configure({
openOnClick: false,
+ }).extend({
+ addMarkView() {
+ return ReactMarkViewRenderer(LinkView);
+ },
}),
Superscript,
SubScript,
diff --git a/apps/client/src/features/editor/hooks/use-long-press.ts b/apps/client/src/features/editor/hooks/use-long-press.ts
new file mode 100644
index 00000000..6e4aaec8
--- /dev/null
+++ b/apps/client/src/features/editor/hooks/use-long-press.ts
@@ -0,0 +1,106 @@
+import { useCallback, useRef } from 'react';
+
+type LongPressOptions = {
+ threshold?: number;
+ onLongPress: (e: React.TouchEvent | React.MouseEvent) => void;
+ onClick?: (e: React.TouchEvent | React.MouseEvent) => void;
+};
+
+type LongPressHandlers = {
+ onMouseDown: (e: React.MouseEvent) => void;
+ onMouseUp: (e: React.MouseEvent) => void;
+ onMouseLeave: (e: React.MouseEvent) => void;
+ onTouchStart: (e: React.TouchEvent) => void;
+ onTouchEnd: (e: React.TouchEvent) => void;
+};
+
+export function useLongPress({
+ threshold = 400,
+ onLongPress,
+ onClick,
+}: LongPressOptions): LongPressHandlers {
+ const timerRef = useRef