@@ -15,8 +15,8 @@ import {
1515 type ThreadPullRequestBadge ,
1616} from "@t3tools/shared/threadPullRequests" ;
1717import { FolderGit2Icon , TerminalIcon } from "lucide-react" ;
18- import { useMemo , type MouseEvent } from "react" ;
19- import { Button , InlineButton } from "./ui/button " ;
18+ import { useRender } from "@base-ui/ react/use-render " ;
19+ import { useMemo , type MouseEvent , type ReactElement } from "react " ;
2020import { cn } from "../lib/utils" ;
2121import { useEnvironment , usePrimaryEnvironmentId } from "../state/environments" ;
2222import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon" ;
@@ -191,17 +191,22 @@ export function resolveThreadPullRequestBadgePresentation({
191191 } ;
192192}
193193
194- /** The complete linked-PR control shared by the sidebar and composer footer. */
194+ /**
195+ * The linked-PR badge shared by the sidebar and composer footer. The badge owns what it shows:
196+ * the state glyph and number at the meta size, in the state's color. The caller owns the control
197+ * it sits in through `render` (an inline link in a sidebar row, a toolbar control in the
198+ * composer), and the badge fills in the link or stack button behavior.
199+ */
195200export function ThreadPullRequestBadgeControl ( {
196- variant ,
201+ render ,
197202 badge,
198203 number,
199204 url,
200205 status,
201206 onOpenStack,
202207 onOpenPullRequest,
203208} : {
204- variant : "underline" | "ghost" ;
209+ render : ReactElement < { render ?: useRender . RenderProp } > ;
205210 badge : ThreadPullRequestBadge | null ;
206211 number ?: number | undefined ;
207212 url ?: string | undefined ;
@@ -211,53 +216,65 @@ export function ThreadPullRequestBadgeControl({
211216} ) {
212217 const presentation = resolveThreadPullRequestBadgePresentation ( { badge, number, url, status } ) ;
213218 if ( presentation === null ) return null ;
214- const isStack = badge ?. kind === "stack" ;
215- const content = (
216- < >
217- < presentation . Icon aria-hidden className = "size-3 shrink-0" />
218- { presentation . text }
219- </ >
219+ return (
220+ < PullRequestBadge
221+ render = { render }
222+ presentation = { presentation }
223+ isStack = { badge ?. kind === "stack" }
224+ url = { url }
225+ onOpenStack = { onOpenStack }
226+ onOpenPullRequest = { onOpenPullRequest }
227+ />
220228 ) ;
221- const linkProps = isStack
222- ? {
223- onClick : ( event : MouseEvent < HTMLElement > ) => {
224- event . preventDefault ( ) ;
225- event . stopPropagation ( ) ;
226- onOpenStack ( ) ;
227- } ,
229+ }
230+
231+ function PullRequestBadge ( {
232+ render,
233+ presentation,
234+ isStack,
235+ url,
236+ onOpenStack,
237+ onOpenPullRequest,
238+ } : {
239+ render : ReactElement < { render ?: useRender . RenderProp } > ;
240+ presentation : NonNullable < ReturnType < typeof resolveThreadPullRequestBadgePresentation > > ;
241+ isStack : boolean ;
242+ url : string | undefined ;
243+ onOpenStack : ( ) => void ;
244+ onOpenPullRequest : ( event : MouseEvent < HTMLElement > ) => void ;
245+ } ) {
246+ const onClick = isStack
247+ ? ( event : MouseEvent < HTMLElement > ) => {
248+ event . preventDefault ( ) ;
249+ event . stopPropagation ( ) ;
250+ onOpenStack ( ) ;
228251 }
229- : { onClick : onOpenPullRequest } ;
252+ : onOpenPullRequest ;
230253 const element = isStack ? (
231254 < button type = "button" />
232255 ) : (
233256 < a href = { url } target = "_blank" rel = "noopener noreferrer" />
234257 ) ;
258+ // The caller's control (InlineButton, ComposerControl) renders as the link or stack button
259+ // through its own render prop; useRender merges the badge's behavior into it.
260+ const control = useRender ( {
261+ render,
262+ props : {
263+ render : element ,
264+ "aria-label" : presentation . label ,
265+ onPointerDown : ( event : MouseEvent < HTMLElement > ) => event . stopPropagation ( ) ,
266+ onClick,
267+ } ,
268+ } ) ;
235269 return (
236270 < Tooltip >
237- < TooltipTrigger
238- render = {
239- variant === "ghost" ? (
240- < Button
241- render = { element }
242- variant = "ghost"
243- size = "xs"
244- className = { presentation . toneClassName }
245- aria-label = { presentation . label }
246- onPointerDown = { ( event ) => event . stopPropagation ( ) }
247- { ...linkProps }
248- />
249- ) : (
250- < InlineButton
251- render = { element }
252- className = { presentation . toneClassName }
253- aria-label = { presentation . label }
254- onPointerDown = { ( event ) => event . stopPropagation ( ) }
255- { ...linkProps }
256- />
257- )
258- }
259- >
260- { content }
271+ < TooltipTrigger render = { control } >
272+ < span
273+ className = { cn ( "contents font-normal text-xs tabular-nums" , presentation . toneClassName ) }
274+ >
275+ < presentation . Icon aria-hidden className = "size-3 shrink-0" />
276+ { presentation . text }
277+ </ span >
261278 </ TooltipTrigger >
262279 < TooltipPopup side = "top" > { presentation . label } </ TooltipPopup >
263280 </ Tooltip >
0 commit comments