SurfSense/surfsense_web/components/ui/link-toolbar.tsx
DESKTOP-RTLN3BA\$punk 64c913baa3 chore: linting
2026-03-27 03:17:05 -07:00

197 lines
4.8 KiB
TypeScript

"use client";
import { flip, offset, type UseVirtualFloatingOptions } from "@platejs/floating";
import { getLinkAttributes } from "@platejs/link";
import {
FloatingLinkUrlInput,
type LinkFloatingToolbarState,
useFloatingLinkEdit,
useFloatingLinkEditState,
useFloatingLinkInsert,
useFloatingLinkInsertState,
} from "@platejs/link/react";
import { cva } from "class-variance-authority";
import { ExternalLink, Link, Text, Unlink } from "lucide-react";
import type { TLinkElement } from "platejs";
import { KEYS } from "platejs";
import {
useEditorRef,
useEditorSelection,
useFormInputProps,
usePluginOption,
} from "platejs/react";
import * as React from "react";
import { buttonVariants } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
const popoverVariants = cva(
"z-50 w-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-hidden"
);
const inputVariants = cva(
"flex h-[28px] w-full rounded-md border-none bg-transparent px-1.5 py-1 text-base placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-transparent md:text-sm"
);
export function LinkFloatingToolbar({ state }: { state?: LinkFloatingToolbarState }) {
const activeCommentId = usePluginOption({ key: KEYS.comment }, "activeId");
const activeSuggestionId = usePluginOption({ key: KEYS.suggestion }, "activeId");
const floatingOptions: UseVirtualFloatingOptions = React.useMemo(
() => ({
middleware: [
offset(8),
flip({
fallbackPlacements: ["bottom-end", "top-start", "top-end"],
padding: 12,
}),
],
placement: activeSuggestionId || activeCommentId ? "top-start" : "bottom-start",
}),
[activeCommentId, activeSuggestionId]
);
const insertState = useFloatingLinkInsertState({
...state,
floatingOptions: {
...floatingOptions,
...state?.floatingOptions,
},
});
const {
hidden,
props: insertProps,
ref: insertRef,
textInputProps,
} = useFloatingLinkInsert(insertState);
const editState = useFloatingLinkEditState({
...state,
floatingOptions: {
...floatingOptions,
...state?.floatingOptions,
},
});
const {
editButtonProps,
props: editProps,
ref: editRef,
unlinkButtonProps,
} = useFloatingLinkEdit(editState);
const inputProps = useFormInputProps({
preventDefaultOnEnterKeydown: true,
});
if (hidden) return null;
const input = (
<div className="flex w-[330px] flex-col" {...inputProps}>
<div className="flex items-center">
<div className="flex items-center pr-1 pl-2 text-muted-foreground">
<Link className="size-4" />
</div>
<FloatingLinkUrlInput
className={inputVariants()}
placeholder="Paste link"
data-plate-focus
/>
</div>
<Separator className="my-1" />
<div className="flex items-center">
<div className="flex items-center pr-1 pl-2 text-muted-foreground">
<Text className="size-4" />
</div>
<input
className={inputVariants()}
placeholder="Text to display"
data-plate-focus
{...textInputProps}
/>
</div>
</div>
);
const editContent = editState.isEditing ? (
input
) : (
<div className="box-content flex items-center">
<button
className={buttonVariants({ size: "sm", variant: "ghost" })}
type="button"
{...editButtonProps}
>
Edit link
</button>
<Separator orientation="vertical" />
<LinkOpenButton />
<Separator orientation="vertical" />
<button
className={buttonVariants({
size: "sm",
variant: "ghost",
})}
type="button"
{...unlinkButtonProps}
>
<Unlink width={18} />
</button>
</div>
);
return (
<>
<div ref={insertRef} className={popoverVariants()} {...insertProps}>
{input}
</div>
<div ref={editRef} className={popoverVariants()} {...editProps}>
{editContent}
</div>
</>
);
}
function LinkOpenButton() {
const editor = useEditorRef();
const selection = useEditorSelection();
// biome-ignore lint/correctness/useExhaustiveDependencies: selection triggers recalculation of link attributes
const attributes = React.useMemo(() => {
const entry = editor.api.node<TLinkElement>({
match: { type: editor.getType(KEYS.link) },
});
if (!entry) {
return {};
}
const [element] = entry;
return getLinkAttributes(editor, element);
}, [editor, selection]);
return (
// biome-ignore lint/a11y/noStaticElementInteractions: <a> with spread attributes has dynamic href
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-label needed for icon-only link
<a
{...attributes}
className={buttonVariants({
size: "sm",
variant: "ghost",
})}
onMouseOver={(e) => {
e.stopPropagation();
}}
onFocus={(e) => {
e.stopPropagation();
}}
aria-label="Open link in a new tab"
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink width={18} />
</a>
);
}