"use client"; /** * MentionView — NodeView for rendering @mentions inline in the editor. * * Member/agent mentions: plain "@Name" text with .mention class styling. * Issue mentions: inline card with StatusIcon + identifier + title. * * Issue card sizing: must fit within the paragraph line box (14px * 1.625 * = 22.75px). Card uses text-xs (12px) + py-0.5 + border ≈ 22px total. * vertical-align: middle is set on the [data-node-view-wrapper] in CSS * (not on the tag) because the wrapper is the outermost inline element * that participates in line box calculation. Setting it on the inner * had no effect since the wrapper was already positioned. * * Fallback: when issue is not in the Zustand store (deleted or other * workspace), the same card style is used with just the identifier from * fallbackLabel — no visual degradation to a plain text link. */ import { NodeViewWrapper } from "@tiptap/react"; import type { NodeViewProps } from "@tiptap/react"; import { useQuery } from "@tanstack/react-query"; import { issueListOptions, issueDetailOptions } from "@multica/core/issues/queries"; import { useWorkspaceId } from "@multica/core/hooks"; import { useWorkspacePaths } from "@multica/core/paths"; import { useNavigation } from "../../navigation"; import { StatusIcon } from "../../issues/components/status-icon"; export function MentionView({ node }: NodeViewProps) { const { type, id, label } = node.attrs; if (type === "issue") { return ( ); } return ( @{label ?? id} ); } function IssueMention({ issueId, fallbackLabel, }: { issueId: string; fallbackLabel?: string; }) { const wsId = useWorkspaceId(); const p = useWorkspacePaths(); const { data: issues = [] } = useQuery(issueListOptions(wsId)); const { push, openInNewTab } = useNavigation(); const listIssue = issues.find((i) => i.id === issueId); const { data: detailIssue } = useQuery({ ...issueDetailOptions(wsId, issueId), enabled: !listIssue, }); const issue = listIssue ?? detailIssue; const issuePath = p.issueDetail(issueId); const tabTitle = issue ? `${issue.identifier}: ${issue.title}` : undefined; const handleClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (e.metaKey || e.ctrlKey || e.shiftKey) { if (openInNewTab) { openInNewTab(issuePath, tabTitle); } return; } push(issuePath); }; const cardClass = "issue-mention inline-flex items-center gap-1.5 rounded-md border mx-0.5 px-2 py-0.5 text-xs hover:bg-accent transition-colors cursor-pointer max-w-72"; if (!issue) { return ( {fallbackLabel ?? issueId.slice(0, 8)} ); } return ( {issue.identifier} {issue.title} ); }