Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
useLinkedThreadPullRequest,
} from "./ThreadStatusIndicators";
import { Button } from "./ui/button";
import { ComposerControl } from "./chat/ComposerControl";
import { Switch } from "./ui/switch";
import { getVirtualizedScrollFadeClassName } from "./ui/scroll-area";
import {
Expand Down Expand Up @@ -790,7 +791,7 @@ export function BranchToolbarBranchSelector({
data-composer-context-control
>
<ThreadPullRequestBadgeControl
variant="ghost"
render={<ComposerControl size="xs" />}
badge={prBadge}
number={prNumber}
url={prUrl}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ import {
} from "../providerInstances";
import { useThreadRunningTerminalIds } from "../state/terminalSessions";
import { stackedThreadToast, toastManager } from "./ui/toast";
import { Button } from "./ui/button";
import { Button, InlineButton } from "./ui/button";
import {
Combobox,
ComboboxEmpty,
Expand Down Expand Up @@ -1510,7 +1510,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
const prBadge =
prBadgeShape?.kind === "stack" || pr || currentLinkedPr ? (
<ThreadPullRequestBadgeControl
variant="underline"
render={<InlineButton />}
badge={prBadgeShape}
number={pr?.number ?? currentLinkedPr?.number}
url={pr?.url ?? currentLinkedPr?.url}
Expand Down
103 changes: 60 additions & 43 deletions apps/web/src/components/ThreadStatusIndicators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
type ThreadPullRequestBadge,
} from "@t3tools/shared/threadPullRequests";
import { FolderGit2Icon, TerminalIcon } from "lucide-react";
import { useMemo, type MouseEvent } from "react";
import { Button, InlineButton } from "./ui/button";
import { useRender } from "@base-ui/react/use-render";
import { useMemo, type MouseEvent, type ReactElement } from "react";
import { cn } from "../lib/utils";
import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";
import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
Expand Down Expand Up @@ -191,17 +191,22 @@ export function resolveThreadPullRequestBadgePresentation({
};
}

/** The complete linked-PR control shared by the sidebar and composer footer. */
/**
* The linked-PR badge shared by the sidebar and composer footer. The badge owns what it shows:
* the state glyph and number at the meta size, in the state's color. The caller owns the control
* it sits in through `render` (an inline link in a sidebar row, a toolbar control in the
* composer), and the badge fills in the link or stack button behavior.
*/
export function ThreadPullRequestBadgeControl({
variant,
render,
badge,
number,
url,
status,
onOpenStack,
onOpenPullRequest,
}: {
variant: "underline" | "ghost";
render: ReactElement<{ render?: useRender.RenderProp }>;
badge: ThreadPullRequestBadge | null;
number?: number | undefined;
url?: string | undefined;
Expand All @@ -211,53 +216,65 @@ export function ThreadPullRequestBadgeControl({
}) {
const presentation = resolveThreadPullRequestBadgePresentation({ badge, number, url, status });
if (presentation === null) return null;
const isStack = badge?.kind === "stack";
const content = (
<>
<presentation.Icon aria-hidden className="size-3 shrink-0" />
{presentation.text}
</>
return (
<PullRequestBadge
render={render}
presentation={presentation}
isStack={badge?.kind === "stack"}
url={url}
onOpenStack={onOpenStack}
onOpenPullRequest={onOpenPullRequest}
/>
);
const linkProps = isStack
? {
onClick: (event: MouseEvent<HTMLElement>) => {
event.preventDefault();
event.stopPropagation();
onOpenStack();
},
}

function PullRequestBadge({
render,
presentation,
isStack,
url,
onOpenStack,
onOpenPullRequest,
}: {
render: ReactElement<{ render?: useRender.RenderProp }>;
presentation: NonNullable<ReturnType<typeof resolveThreadPullRequestBadgePresentation>>;
isStack: boolean;
url: string | undefined;
onOpenStack: () => void;
onOpenPullRequest: (event: MouseEvent<HTMLElement>) => void;
}) {
const onClick = isStack
? (event: MouseEvent<HTMLElement>) => {
event.preventDefault();
event.stopPropagation();
onOpenStack();
}
: { onClick: onOpenPullRequest };
: onOpenPullRequest;
const element = isStack ? (
<button type="button" />
) : (
<a href={url} target="_blank" rel="noopener noreferrer" />
);
// The caller's control (InlineButton, ComposerControl) renders as the link or stack button
// through its own render prop; useRender merges the badge's behavior into it.
const control = useRender({
render,
props: {
render: element,
"aria-label": presentation.label,
onPointerDown: (event: MouseEvent<HTMLElement>) => event.stopPropagation(),
onClick,
},
});
return (
<Tooltip>
<TooltipTrigger
render={
variant === "ghost" ? (
<Button
render={element}
variant="ghost"
size="xs"
className={presentation.toneClassName}
aria-label={presentation.label}
onPointerDown={(event) => event.stopPropagation()}
{...linkProps}
/>
) : (
<InlineButton
render={element}
className={presentation.toneClassName}
aria-label={presentation.label}
onPointerDown={(event) => event.stopPropagation()}
{...linkProps}
/>
)
}
>
{content}
<TooltipTrigger render={control}>
<span
className={cn("contents font-normal text-xs tabular-nums", presentation.toneClassName)}
>
<presentation.Icon aria-hidden className="size-3 shrink-0" />
{presentation.text}
</span>
</TooltipTrigger>
<TooltipPopup side="top">{presentation.label}</TooltipPopup>
</Tooltip>
Expand Down
2 changes: 1 addition & 1 deletion scripts/lint-restyle-ceiling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as NodeURL from "node:url";
// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the
// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you
// migrate a file, and delete this script when the rule becomes an error.
export const RESTYLE_CEILING = 628;
export const RESTYLE_CEILING = 627;

const RULE = "shadcn(no-restyle)";

Expand Down
Loading