refactor(views): give a project mention the component an issue mention has

`IssueMentionCard` owns "chip inside a link" for issues; the project equivalent
lived inline in the readonly renderer, so nothing named the pairing and nothing
held the rules that come with being a link.

That cost was not hypothetical. Both gaps fixed a commit ago landed on project
mentions alone: `.project-mention` never got the CSS rule cancelling generic
link chrome, and the hover card never learned to skip it. Each was written for
`.issue-mention` at the component that owns it, and project had no such place
for the second half to be written. `ProjectMentionCard` is that place.

No behaviour change: same anchor, same href, same hover affordance, same
accessibility contract that project-mention-a11y.test.tsx pins. The "open in
new tab" preference stays out — it is scoped to issue links, and inheriting it
by symmetry would be inventing product.

Also drops `not-prose` from both cards. It has no definition anywhere in the
repo — Tailwind's typography plugin is not installed, and the class does not
appear in built CSS — so it read as protection that was not there.

The editor's `MentionView` keeps its hand-rolled anchors: it needs a
modifier-click intent hook `AppLink` does not expose, and it does the same for
issues, so the two stay symmetric there too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing
2026-07-31 16:05:46 +08:00
parent 7bf76be012
commit 21267745ee
4 changed files with 62 additions and 28 deletions

View File

@@ -27,7 +27,7 @@ export function IssueMentionCard({ issueId, fallbackLabel }: IssueMentionCardPro
href={p.issueDetail(issueId)}
target={openInNewTab ? "_blank" : undefined}
newTabTitle={fallbackLabel}
className="issue-mention not-prose align-middle"
className="issue-mention align-middle"
>
<IssueChip
issueId={issueId}

View File

@@ -2,4 +2,5 @@ export { ProjectsPage } from "./projects-page";
export { ProjectDetail } from "./project-detail";
export { ProjectPicker } from "./project-picker";
export { ProjectChip } from "./project-chip";
export { ProjectMentionCard } from "./project-mention-card";
export { LocalDirectoryHint } from "./local-directory-hint";

View File

@@ -0,0 +1,52 @@
"use client";
import { AppLink } from "../../navigation";
import { useWorkspacePaths } from "@multica/core/paths";
import { ProjectChip } from "./project-chip";
interface ProjectMentionCardProps {
projectId: string;
/** Fallback text when the project is not resolvable (e.g. its title). */
fallbackLabel?: string;
}
/**
* Navigable chip — wraps ProjectChip in an AppLink pointing at the project's
* detail page. Counterpart of IssueMentionCard, and deliberately shaped like
* it: `ProjectChip` is presentational, so whatever wraps it decides that a
* mention is a link, and that decision belongs in one place.
*
* It was in none until now — the wrapper lived inline in the readonly
* renderer — and the cost showed up as gaps that landed on project mentions
* alone: `.project-mention` never got the CSS rule cancelling generic link
* chrome, and the link hover card never learned to skip it. A named component
* is where the next such rule has somewhere obvious to go.
*
* Rendered as a real anchor: a mention must be reachable by Tab, activatable by
* Enter, and expose a URL to copy or open in a new tab. A `<span onClick>`
* looks identical and works for a mouse, which is exactly why that regression
* is easy to miss (see project-mention-a11y.test.tsx).
*
* AppLink owns plain-click, modifier-click and the desktop new-tab adapter, so
* none of that is reimplemented here. Unlike an issue there is no "open in new
* tab" preference to honour — that setting is scoped to issue links.
*/
export function ProjectMentionCard({
projectId,
fallbackLabel,
}: ProjectMentionCardProps) {
const p = useWorkspacePaths();
return (
<AppLink
href={p.projectDetail(projectId)}
newTabTitle={fallbackLabel}
className="project-mention inline-flex"
>
<ProjectChip
projectId={projectId}
fallbackLabel={fallbackLabel}
className="cursor-pointer hover:bg-accent transition-colors"
/>
</AppLink>
);
}

View File

@@ -39,7 +39,7 @@ import remarkMath from "remark-math";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import { cn } from "@multica/ui/lib/utils";
import { useWorkspacePaths, useWorkspaceSlug } from "@multica/core/paths";
import { useWorkspaceSlug } from "@multica/core/paths";
import { useConfigStore } from "@multica/core/config";
import type { Attachment } from "@multica/core/types";
import {
@@ -48,10 +48,10 @@ import {
markdownSanitizeSchema,
markdownUrlTransform,
} from "@multica/ui/markdown";
import { AppLink, useAppOrigin } from "../navigation";
import { useAppOrigin } from "../navigation";
import { IssueMentionCard } from "../issues/components/issue-mention-card";
import { useResolveIssueIdentifier } from "../issues/hooks";
import { ProjectChip } from "../projects/components/project-chip";
import { ProjectMentionCard } from "../projects/components/project-mention-card";
import { useLinkHover, LinkHoverCard } from "../editor/link-hover-card";
import {
openLink,
@@ -144,34 +144,15 @@ function IdentifierIssueMentionLink({
}
/**
* Project mention chip.
*
* Rendered as a real anchor via AppLink, matching IssueMentionCard: a mention is
* a link, so it must be reachable by Tab and activatable by Enter, and expose a
* URL to copy / open in a new tab. A `<span onClick>` looks identical and works
* for a mouse, which is exactly why the regression is easy to miss — the readonly
* renderer used one, and unifying the surfaces would have propagated it to Chat,
* which previously had the accessible version.
*
* AppLink also owns plain-click, modifier-click and the desktop new-tab adapter,
* so none of that is reimplemented here. The wrapper only shields surrounding
* click handlers (e.g. collapsed-comment expanders) from mention clicks.
* Project mention chip. Navigation and accessibility are owned by the AppLink
* inside ProjectMentionCard; the wrapper only shields surrounding click
* handlers (e.g. collapsed-comment expanders) from mention clicks — the same
* shape as IssueMentionLink above.
*/
function ProjectMentionLink({ projectId, label }: { projectId: string; label?: string }) {
const p = useWorkspacePaths();
return (
<span className="inline align-middle" onClick={(e) => e.stopPropagation()}>
<AppLink
href={p.projectDetail(projectId)}
newTabTitle={label}
className="project-mention not-prose inline-flex"
>
<ProjectChip
projectId={projectId}
fallbackLabel={label}
className="cursor-pointer hover:bg-accent transition-colors"
/>
</AppLink>
<ProjectMentionCard projectId={projectId} fallbackLabel={label} />
</span>
);
}