Files
multica/packages/views/issues/components/issue-chip.tsx
Naiyuan Qing 4c7fefd143 fix(editor): cap issue mention chip at container width, no clickable gap (#4295)
#4288 swapped the chip cap from a fixed `max-w-72` to `max-w-[min(18rem,100%)]`.
A percentage max-width on a flex item is dropped while its flex-container wrapper
(`<a class="inline-flex">`) computes its own max-content size, so the wrapper
ballooned to the untruncated title width while the chip truncated to the cap —
leaving an empty, clickable strip after the visible chip.

Fix it as standard atomic-inline behavior instead of a fixed magic cap:
- IssueChip caps at `max-w-full` with the title truncating to an ellipsis, so it
  wraps to the next line as a unit and only truncates once a whole line can't
  hold it.
- Drop `inline-flex` from the editor NodeView `<a>` (issue + project) and the
  markdown AppLink so the chip's only wrapper is a plain inline box with a
  definite (line-based) percentage basis — no second flex container to balloon.

ProjectChip uses a definite `max-w-72`, so it never hit the gap; left as-is.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:11:07 +08:00

73 lines
2.9 KiB
TypeScript

"use client";
import { useQuery } from "@tanstack/react-query";
import { issueListOptions, issueDetailOptions } from "@multica/core/issues/queries";
import { useWorkspaceId } from "@multica/core/hooks";
import { StatusIcon } from "./status-icon";
/**
* Compact, presentation-only representation of an issue —
* `<StatusIcon> <identifier> <title>`, bordered, capped at the container width
* (`max-w-full`) with the title truncating to an ellipsis. As an atomic inline
* box it wraps to the next line as a unit when it doesn't fit at the current
* position; the ellipsis only kicks in once a whole line can't hold it. The cap
* lives here (single source of truth) — wrappers must NOT add their own flex
* container around it, or a percentage cap gets dropped during the wrapper's
* intrinsic sizing and the clickable box diverges from the truncated chip.
*
* This is the single source of truth for the "issue-mention card" look.
* It is intentionally **not** a link or button: callers wrap it in whatever
* interactive shell they need (AppLink for markdown mentions, an <a> with
* cmd-click support inside the editor's NodeView, a plain span next to a
* dismiss button in chat's context anchor card, …).
*
* Size budget: must fit within a 14px line-box when used inline — hence
* `py-0.5` + text-xs (see MentionView docstring for the math).
*/
export interface IssueChipProps {
issueId: string;
/** Shown when the issue can't be resolved (deleted, other workspace, …). */
fallbackLabel?: string;
/** Extra classes — callers layer interaction hints here
* (e.g. `hover:bg-accent cursor-pointer` for navigable variants). */
className?: string;
}
const BASE_CLASS =
"issue-mention inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-md border mx-0.5 px-2 py-0.5 text-xs";
export function IssueChip({ issueId, fallbackLabel, className }: IssueChipProps) {
const wsId = useWorkspaceId();
const { data: issues = [] } = useQuery(issueListOptions(wsId));
const listIssue = issues.find((i) => i.id === issueId);
// Fallback fetch for issues outside the first page of the list (e.g. Done).
const { data: detailIssue } = useQuery({
...issueDetailOptions(wsId, issueId),
enabled: !listIssue,
});
const issue = listIssue ?? detailIssue;
const cls = className ? `${BASE_CLASS} ${className}` : BASE_CLASS;
if (!issue) {
return (
<span className={cls}>
<span className="min-w-0 truncate font-medium text-muted-foreground">
{fallbackLabel ?? issueId.slice(0, 8)}
</span>
</span>
);
}
return (
<span className={cls}>
<StatusIcon status={issue.status} className="h-3.5 w-3.5 shrink-0" />
<span className="font-medium text-muted-foreground shrink-0">
{issue.identifier}
</span>
<span className="min-w-0 truncate text-foreground">{issue.title}</span>
</span>
);
}