feat: polish issue thread markdown and references

This commit is contained in:
Dotta 2026-04-10 22:26:21 -05:00
parent 548721248e
commit 958c11699e
16 changed files with 659 additions and 44 deletions

View file

@ -1,14 +1,22 @@
import { isValidElement, useEffect, useId, useState, type ReactNode } from "react";
import { useQuery } from "@tanstack/react-query";
import Markdown, { type Components } from "react-markdown";
import remarkGfm from "remark-gfm";
import { cn } from "../lib/utils";
import { useTheme } from "../context/ThemeContext";
import { mentionChipInlineStyle, parseMentionChipHref } from "../lib/mention-chips";
import { issuesApi } from "../api/issues";
import { queryKeys } from "../lib/queryKeys";
import { Link } from "@/lib/router";
import { parseIssueReferenceFromHref, remarkLinkIssueReferences } from "../lib/issue-reference";
import { remarkSoftBreaks } from "../lib/remark-soft-breaks";
import { StatusIcon } from "./StatusIcon";
interface MarkdownBodyProps {
children: string;
className?: string;
style?: React.CSSProperties;
softBreaks?: boolean;
/** Optional resolver for relative image paths (e.g. within export packages) */
resolveImageSrc?: (src: string) => string | null;
/** Called when a user clicks an inline image */
@ -17,6 +25,29 @@ interface MarkdownBodyProps {
let mermaidLoaderPromise: Promise<typeof import("mermaid").default> | null = null;
function MarkdownIssueLink({
issuePathId,
href,
children,
}: {
issuePathId: string;
href: string;
children: ReactNode;
}) {
const { data } = useQuery({
queryKey: queryKeys.issues.detail(issuePathId),
queryFn: () => issuesApi.get(issuePathId),
staleTime: 60_000,
});
return (
<Link to={href} className="inline-flex items-center gap-1.5 align-baseline">
{data ? <StatusIcon status={data.status} className="h-3.5 w-3.5" /> : null}
<span>{children}</span>
</Link>
);
}
function loadMermaid() {
if (!mermaidLoaderPromise) {
mermaidLoaderPromise = import("mermaid").then((module) => module.default);
@ -94,8 +125,11 @@ function MermaidDiagramBlock({ source, darkMode }: { source: string; darkMode: b
);
}
export function MarkdownBody({ children, className, style, resolveImageSrc, onImageClick }: MarkdownBodyProps) {
export function MarkdownBody({ children, className, style, softBreaks = true, resolveImageSrc, onImageClick }: MarkdownBodyProps) {
const { theme } = useTheme();
const remarkPlugins = softBreaks
? [remarkGfm, remarkLinkIssueReferences, remarkSoftBreaks]
: [remarkGfm, remarkLinkIssueReferences];
const components: Components = {
pre: ({ node: _node, children: preChildren, ...preProps }) => {
const mermaidSource = extractMermaidSource(preChildren);
@ -105,6 +139,15 @@ export function MarkdownBody({ children, className, style, resolveImageSrc, onIm
return <pre {...preProps}>{preChildren}</pre>;
},
a: ({ href, children: linkChildren }) => {
const issueRef = parseIssueReferenceFromHref(href);
if (issueRef) {
return (
<MarkdownIssueLink issuePathId={issueRef.issuePathId} href={issueRef.href}>
{linkChildren}
</MarkdownIssueLink>
);
}
const parsed = href ? parseMentionChipHref(href) : null;
if (parsed) {
const targetHref = parsed.kind === "project"
@ -159,7 +202,7 @@ export function MarkdownBody({ children, className, style, resolveImageSrc, onIm
)}
style={style}
>
<Markdown remarkPlugins={[remarkGfm]} components={components} urlTransform={(url) => url}>
<Markdown remarkPlugins={remarkPlugins} components={components} urlTransform={(url) => url}>
{children}
</Markdown>
</div>