ReferenceNode.tsx2.6 KBView on GitHub 'use client';
import { Node, mergeAttributes } from '@tiptap/core';
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
import type { NodeViewProps } from '@tiptap/react';
import { cn } from '@/lib/utils';
import { referenceMeta, referenceLabel, isOpenableReference } from './references';
export interface ReferenceNodeOptions {
/** Invoked when an openable `@` reference chip is clicked, with its path. */
onOpen: ((path: string) => void) | null;
}
/**
* Inline atom chip representing a playbook `@` reference (e.g.
* `@resources/templates#discovery-post-demo`, `@crm-updater`,
* `@knowledge-base/company-background`). Stores only the `path`; icon and accent
* are derived from the namespace. Openable references (everything except the
* `@crm-updater` / `@next-steps` system tokens) are clickable and call
* `options.onOpen` to open the underlying document.
*/
function ReferenceNodeView({ node, extension }: NodeViewProps) {
const path = (node.attrs.path as string | null) ?? '';
const meta = referenceMeta(path);
const Icon = meta.icon;
const openable = isOpenableReference(path);
const onOpen = (extension.options as ReferenceNodeOptions).onOpen;
return (
<NodeViewWrapper
as="span"
data-reference-node=""
title={openable ? `Open ${path}` : undefined}
onClick={openable && onOpen ? () => onOpen(path) : undefined}
className={cn(
'mx-0.5 inline-flex select-none items-center gap-1 rounded-full px-2 py-0.5 align-baseline text-xs font-medium leading-none transition-colors',
meta.className,
openable && onOpen && 'cursor-pointer hover:underline',
)}
contentEditable={false}
>
<Icon className="h-3 w-3 shrink-0" />
<span className="truncate">{referenceLabel(path)}</span>
</NodeViewWrapper>
);
}
export const ReferenceNode = Node.create<ReferenceNodeOptions>({
name: 'referenceNode',
group: 'inline',
inline: true,
atom: true,
selectable: true,
draggable: false,
addOptions() {
return { onOpen: null };
},
addAttributes() {
return {
path: {
default: '' as string,
parseHTML: (element) => element.getAttribute('data-reference-path'),
renderHTML: (attributes) =>
attributes.path ? { 'data-reference-path': attributes.path } : {},
},
};
},
parseHTML() {
return [{ tag: 'span[data-reference-node]' }];
},
renderHTML({ HTMLAttributes }) {
return ['span', mergeAttributes({ 'data-reference-node': '' }, HTMLAttributes)];
},
addNodeView() {
return ReactNodeViewRenderer(ReferenceNodeView);
},
});