Skip to content

Commit b6fbe37

Browse files
refactor(web): InlineButton underlines on hover and takes a tone
Six of eight InlineButton sites re-declared font-medium underline-offset-2 hover:underline plus a tone, each slightly differently; the other two drew a bottom border instead. An inline text action has one look now: surrounding text size, medium weight, underline on hover, with tone="muted|destructive" for the two tones in use. It also accepts render, so a link can be an inline action. The thread pull-request badge drops buttonVariants and renders through Button or InlineButton, and the tabular-nums/11px tweaks on it go. Visible effect: the sidebar undo link underlines instead of drawing a border; the two process signal links and the expandable-text toggle read at surrounding size instead of 11px; the sidebar PR badge no longer forces tabular figures. Lowers the no-restyle ceiling from 1038 to 999. Co-Authored-By: Claude Fable 5 <[email protected]>
1 parent f974f5e commit b6fbe37

9 files changed

Lines changed: 65 additions & 62 deletions

File tree

‎apps/web/src/components/Sidebar.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1374,7 +1374,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
13741374
if (!showSnoozeButton) setSnoozeMenuOpen(false);
13751375
}, [showSnoozeButton]);
13761376
const handlePrClick = useCallback(
1377-
(event: ReactMouseEvent<HTMLAnchorElement>) => {
1377+
(event: ReactMouseEvent<HTMLElement>) => {
13781378
const url = pr?.url ?? currentLinkedPr?.url;
13791379
if (!url) return;
13801380
const openedInRightPanel = openPrLink(

‎apps/web/src/components/ThreadStatusIndicators.tsx‎

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from "@t3tools/shared/threadPullRequests";
1717
import { FolderGit2Icon, TerminalIcon } from "lucide-react";
1818
import { useMemo, type MouseEvent } from "react";
19-
import { buttonVariants, InlineButton } from "./ui/button";
19+
import { Button, InlineButton } from "./ui/button";
2020
import { cn } from "../lib/utils";
2121
import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";
2222
import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
@@ -207,50 +207,52 @@ export function ThreadPullRequestBadgeControl({
207207
url?: string | undefined;
208208
status: PrStatusIndicator | null;
209209
onOpenStack: () => void;
210-
onOpenPullRequest: (event: MouseEvent<HTMLAnchorElement>) => void;
210+
onOpenPullRequest: (event: MouseEvent<HTMLElement>) => void;
211211
}) {
212212
const presentation = resolveThreadPullRequestBadgePresentation({ badge, number, url, status });
213213
if (presentation === null) return null;
214214
const isStack = badge?.kind === "stack";
215-
const className = cn(
216-
variant === "ghost"
217-
? buttonVariants({ variant: "ghost", size: "xs" })
218-
: "inline-flex shrink-0 cursor-pointer items-center gap-0.5 whitespace-nowrap border-b border-transparent hover:border-current focus-visible:outline-2 focus-visible:outline-ring",
219-
"text-xs tabular-nums",
220-
variant === "ghost" &&
221-
"font-normal text-xs! active:scale-100 [--control-icon-color:currentColor]",
222-
presentation.toneClassName,
223-
);
224215
const content = (
225216
<>
226217
<presentation.Icon aria-hidden className="size-3 shrink-0" />
227218
{presentation.text}
228219
</>
229220
);
221+
const linkProps = isStack
222+
? {
223+
onClick: (event: MouseEvent<HTMLElement>) => {
224+
event.preventDefault();
225+
event.stopPropagation();
226+
onOpenStack();
227+
},
228+
}
229+
: { onClick: onOpenPullRequest };
230+
const element = isStack ? (
231+
<button type="button" />
232+
) : (
233+
<a href={url} target="_blank" rel="noopener noreferrer" />
234+
);
230235
return (
231236
<Tooltip>
232237
<TooltipTrigger
233238
render={
234-
isStack ? (
235-
<InlineButton
236-
className={className}
239+
variant === "ghost" ? (
240+
<Button
241+
render={element}
242+
variant="ghost"
243+
size="xs"
244+
className={presentation.toneClassName}
237245
aria-label={presentation.label}
238246
onPointerDown={(event) => event.stopPropagation()}
239-
onClick={(event) => {
240-
event.preventDefault();
241-
event.stopPropagation();
242-
onOpenStack();
243-
}}
247+
{...linkProps}
244248
/>
245249
) : (
246-
<a
247-
href={url}
248-
target="_blank"
249-
rel="noopener noreferrer"
250-
className={className}
250+
<InlineButton
251+
render={element}
252+
className={presentation.toneClassName}
251253
aria-label={presentation.label}
252254
onPointerDown={(event) => event.stopPropagation()}
253-
onClick={onOpenPullRequest}
255+
{...linkProps}
254256
/>
255257
)
256258
}

‎apps/web/src/components/pullRequest/PullRequestGhosts.tsx‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,7 @@ export function PullRequestDetailGhost({
169169
<span className="min-w-0 truncate font-medium">{seed.repository}</span>
170170
<InlineButton
171171
onClick={() => void readLocalApi()?.shell.openExternal(seed.url)}
172-
className={cn(
173-
"font-medium underline-offset-2 hover:underline",
174-
statePresentation?.toneClassName,
175-
)}
172+
className={statePresentation?.toneClassName}
176173
aria-label={`Open pull request #${seed.number} on host`}
177174
>
178175
#{seed.number}

‎apps/web/src/components/settings/ExpandableText.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export function ExpandableText({
3737
<InlineButton
3838
aria-expanded={expanded}
3939
aria-controls={textId}
40-
className="mt-1 text-[11px] font-medium text-foreground/70 underline-offset-2 hover:text-foreground hover:underline"
40+
tone="muted"
41+
className="mt-1"
4142
onClick={() => setExpanded((value) => !value)}
4243
>
4344
{expanded ? "Show less" : expandLabel}

‎apps/web/src/components/settings/ProcessSignalActions.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function ProcessSignalActions({
1919
<InlineButton
2020
disabled={disabled}
2121
aria-label="Send SIGINT"
22-
className="text-[11px] font-medium text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
22+
tone="muted"
2323
onClick={() => onSignal("SIGINT")}
2424
>
2525
INT
@@ -34,7 +34,7 @@ export function ProcessSignalActions({
3434
<InlineButton
3535
disabled={disabled}
3636
aria-label="Send SIGKILL"
37-
className="text-[11px] font-medium text-destructive underline-offset-2 hover:underline"
37+
tone="destructive"
3838
onClick={() => onSignal("SIGKILL")}
3939
>
4040
KILL

‎apps/web/src/components/settings/SettingInheritance.tsx‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -286,10 +286,7 @@ export function SettingInheritance({
286286
<div className="flex items-center justify-between gap-3 px-2 text-xs text-muted-foreground">
287287
<span>Overridden by</span>
288288
{onClearOverrides ? (
289-
<InlineButton
290-
className="font-medium text-foreground underline-offset-2 hover:underline"
291-
onClick={() => onClearOverrides(overriding)}
292-
>
289+
<InlineButton onClick={() => onClearOverrides(overriding)}>
293290
Reset {overriding.length === 1 ? "it" : "all"}
294291
</InlineButton>
295292
) : null}
@@ -300,10 +297,7 @@ export function SettingInheritance({
300297
key={project.projectId}
301298
className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 px-2 py-1"
302299
>
303-
<InlineButton
304-
className="min-w-0 justify-start text-left text-foreground underline-offset-2 hover:underline"
305-
onClick={project.open}
306-
>
300+
<InlineButton className="min-w-0 justify-start" onClick={project.open}>
307301
<span className="truncate">{project.label}</span>
308302
</InlineButton>
309303
<span className="max-w-32 truncate text-muted-foreground tabular-nums">

‎apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@ export function SidebarThreadUndoNotice() {
1717
<Alert role="status" variant="sidebar">
1818
<AlertDescription>
1919
{notice.action} {notice.count} thread{notice.count === 1 ? "" : "s"},{" "}
20-
<InlineButton
21-
underline
22-
onClick={undoLatestThreadAction}
23-
className="hover:text-sidebar-foreground"
24-
>
20+
<InlineButton onClick={undoLatestThreadAction}>
2521
{shortcut ? `${shortcut} to undo` : "Undo"}
2622
</InlineButton>
2723
</AlertDescription>

‎apps/web/src/components/ui/button.tsx‎

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,35 @@ function Button({ className, variant, size, render, ...props }: ButtonProps) {
9797

9898
export { Button, buttonVariants };
9999

100-
/** An inline action that keeps the geometry of surrounding text or a graph node. */
100+
const inlineButtonVariants = cva(
101+
"inline-flex shrink-0 cursor-pointer items-center gap-0.5 whitespace-nowrap font-medium underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-64",
102+
{
103+
defaultVariants: { tone: "default" },
104+
variants: {
105+
tone: {
106+
default: "text-foreground",
107+
muted: "text-muted-foreground hover:text-foreground",
108+
destructive: "text-destructive/80 hover:text-destructive",
109+
},
110+
},
111+
},
112+
);
113+
114+
/** An inline text action that keeps the size of the surrounding text and underlines on hover. */
101115
export function InlineButton({
102116
className,
103-
underline = false,
117+
tone,
118+
render,
104119
...props
105-
}: React.ComponentProps<"button"> & { underline?: boolean }) {
106-
return (
107-
<button
108-
type="button"
109-
data-slot="inline-button"
110-
className={cn(
111-
"inline-flex shrink-0 cursor-pointer items-center gap-0.5 whitespace-nowrap focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-64",
112-
underline && "border-b border-transparent hover:border-current",
113-
className,
114-
)}
115-
{...props}
116-
/>
117-
);
120+
}: useRender.ComponentProps<"button"> & VariantProps<typeof inlineButtonVariants>) {
121+
const defaultProps = {
122+
className: cn(inlineButtonVariants({ tone }), className),
123+
"data-slot": "inline-button",
124+
type: render ? undefined : ("button" as const),
125+
};
126+
return useRender({
127+
defaultTagName: "button",
128+
props: mergeProps<"button">(defaultProps, props),
129+
render,
130+
});
118131
}

‎scripts/lint-restyle-ceiling.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";
88
// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the
99
// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you
1010
// migrate a file, and delete this script when the rule becomes an error.
11-
export const RESTYLE_CEILING = 870;
11+
export const RESTYLE_CEILING = 831;
1212

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

0 commit comments

Comments
 (0)