Skip to content

Commit c9e6590

Browse files
samejrclaude
andcommitted
refactor(webapp): route hand-rolled links through TextLink
Links that underlined themselves now either use TextLink or share its colour and the marker the "Underline links" preference targets, so the preference reaches them. TextLink gains `textLinkClassName` for the cases that can't be the component - markdown links that must stay in the inline flow, and triggers that aren't anchors - plus `reloadDocument` so LabelValueStack keeps its behaviour. Deletes the ProductHunt banner and its image, which nothing rendered. Left alone: the admin pages, and the underlines that aren't links (dashed tooltip terms, the dotted Vercel warning, URL-as-text, the focus affordance). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent f2adfeb commit c9e6590

14 files changed

Lines changed: 78 additions & 86 deletions

File tree

-5.54 KB
Binary file not shown.

apps/webapp/app/components/ProductHuntBanner.tsx

Lines changed: 0 additions & 24 deletions
This file was deleted.

apps/webapp/app/components/billing/BillingLimitConfigSection.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Paragraph } from "~/components/primitives/Paragraph";
2020
import { RadioGroup, RadioGroupItem } from "~/components/primitives/RadioButton";
2121
import type { BillingLimitResult } from "~/services/billingLimit.schemas";
2222
import { formatCurrency } from "~/utils/numberFormatter";
23+
import { TextLink } from "~/components/primitives/TextLink";
2324

2425
export const billingLimitFormSchema = z.discriminatedUnion("mode", [
2526
z.object({
@@ -338,10 +339,7 @@ function LimitReachedCalloutContent({
338339
When this limit is reached, queued runs will be held for {gracePeriodLabel}, then new triggers
339340
will be rejected until you increase or remove the limit. Limits are enforced with a short
340341
delay, so spend may briefly exceed the limit before grace begins. See our{" "}
341-
<a href="https://trigger.dev/terms" className="underline">
342-
terms
343-
</a>{" "}
344-
for refund policy details.
342+
<TextLink href="https://trigger.dev/terms">terms</TextLink> for refund policy details.
345343
{cancelInProgressRuns ? (
346344
<> In-progress runs will be cancelled when the limit is hit.</>
347345
) : null}

apps/webapp/app/components/dashboard-agent/RunDiagnosisCard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useOptionalOrganization } from "~/hooks/useOrganizations";
77
import { useOptionalProject } from "~/hooks/useProject";
88
import { cn } from "~/utils/cn";
99
import { v3RunPath } from "~/utils/pathBuilder";
10+
import { textLinkClassName } from "~/components/primitives/TextLink";
1011

1112
// The "why did this run fail?" failure card — the first block in the dashboard
1213
// agent's view catalog. Rendered from a `diagnosis` block the agent emits via
@@ -60,7 +61,7 @@ function RunLink({ runId, className }: { runId: string; className?: string }) {
6061
const to = useRunPath(runId);
6162
if (!to) return <span className={cn("font-mono text-text-dimmed", className)}>{runId}</span>;
6263
return (
63-
<Link to={to} className={cn("text-indigo-400 underline hover:text-indigo-300", className)}>
64+
<Link to={to} className={cn(textLinkClassName(), className)}>
6465
{runId}
6566
</Link>
6667
);
@@ -80,7 +81,7 @@ function EvidenceReference({ reference }: { reference: string }) {
8081
href={safeUrl}
8182
target="_blank"
8283
rel="noopener noreferrer"
83-
className="font-mono text-xs text-indigo-400 underline hover:text-indigo-300"
84+
className={cn(textLinkClassName(), "font-mono text-xs")}
8485
>
8586
{reference}
8687
</a>

apps/webapp/app/components/navigation/NotificationCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { XMarkIcon } from "@heroicons/react/20/solid";
22
import { useLayoutEffect, useRef, useState } from "react";
33
import ReactMarkdown from "react-markdown";
44
import { cn } from "~/utils/cn";
5+
import { textLinkClassName } from "~/components/primitives/TextLink";
56

67
export function NotificationCard({
78
title,
@@ -109,7 +110,7 @@ function getMarkdownComponents(onLinkClick?: () => void) {
109110
href={href}
110111
target="_blank"
111112
rel="noopener noreferrer"
112-
className="relative z-20 text-indigo-400 underline transition-colors hover:text-indigo-300"
113+
className={cn(textLinkClassName(), "relative z-20")}
113114
onClick={(e) => {
114115
e.stopPropagation();
115116
onLinkClick?.();

apps/webapp/app/components/primitives/LabelValueStack.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { cn } from "~/utils/cn";
22
import { Paragraph } from "./Paragraph";
3+
import { TextLink } from "./TextLink";
34
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid";
45
import { SimpleTooltip } from "./Tooltip";
5-
import { Link } from "@remix-run/react";
66

77
const variations = {
88
primary: {
@@ -69,9 +69,9 @@ function ValueButton({ value, href, variant = "secondary" }: ValueButtonStackPro
6969
if (!isExternalUrl) {
7070
return (
7171
<Paragraph variant={variation.value}>
72-
<Link to={href} reloadDocument className="underline underline-offset-2">
72+
<TextLink to={href} reloadDocument>
7373
{value}
74-
</Link>
74+
</TextLink>
7575
</Paragraph>
7676
);
7777
}
@@ -81,10 +81,14 @@ function ValueButton({ value, href, variant = "secondary" }: ValueButtonStackPro
8181
side="bottom"
8282
button={
8383
<Paragraph variant={variation.value}>
84-
<a href={href} className="underline underline-offset-2" target="_blank">
84+
<TextLink
85+
href={href}
86+
target="_blank"
87+
trailingIcon={ArrowTopRightOnSquareIcon}
88+
trailingIconClassName="text-text-dimmed"
89+
>
8590
{value}
86-
<ArrowTopRightOnSquareIcon className="ml-1 inline-block h-4 w-4 text-text-dimmed" />
87-
</a>
91+
</TextLink>
8892
</Paragraph>
8993
}
9094
content={href}

apps/webapp/app/components/primitives/TextLink.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,26 @@ import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKey
66
import { ShortcutKey } from "./ShortcutKey";
77
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip";
88

9-
// inline-text-link: marker for the "Underline links" preference, which underlines
10-
// these and nothing else (nav items, buttons-as-links and decorative underlines
11-
// all stay put). See tailwind.css.
12-
const base = "inline-text-link inline-flex gap-0.5 items-center group focus-visible:focus-custom";
9+
const colors = {
10+
primary: "text-indigo-500 transition hover:text-indigo-400",
11+
secondary: "text-text-dimmed transition hover:text-text-bright",
12+
} as const;
13+
14+
/**
15+
* A link's colour plus `inline-text-link`, the marker the "Underline links"
16+
* preference targets (see tailwind.css) - without this component's layout.
17+
*
18+
* For links that can't be a `TextLink`: ones that must stay in the inline flow
19+
* (markdown prose, where the component's inline-flex would stop them wrapping),
20+
* and triggers that aren't anchors at all.
21+
*/
22+
export function textLinkClassName(variant: keyof typeof colors = "primary") {
23+
return cn("inline-text-link focus-visible:focus-custom", colors[variant]);
24+
}
1325

1426
const variations = {
15-
primary: `${base} text-indigo-500 transition hover:text-indigo-400`,
16-
secondary: `${base} text-text-dimmed transition hover:text-text-bright`,
27+
primary: cn(textLinkClassName("primary"), "inline-flex gap-0.5 items-center group"),
28+
secondary: cn(textLinkClassName("secondary"), "inline-flex gap-0.5 items-center group"),
1729
} as const;
1830

1931
type TextLinkProps = {
@@ -27,6 +39,8 @@ type TextLinkProps = {
2739
shortcut?: ShortcutDefinition;
2840
hideShortcutKey?: boolean;
2941
tooltip?: React.ReactNode;
42+
/** Forwarded to `Link`: forces a full document load rather than a client nav. */
43+
reloadDocument?: boolean;
3044
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
3145

3246
export function TextLink({
@@ -40,6 +54,7 @@ export function TextLink({
4054
shortcut,
4155
hideShortcutKey,
4256
tooltip,
57+
reloadDocument,
4358
...props
4459
}: TextLinkProps) {
4560
const innerRef = useRef<HTMLAnchorElement>(null);
@@ -69,7 +84,13 @@ export function TextLink({
6984
);
7085

7186
const linkElement = to ? (
72-
<Link ref={innerRef} to={to} className={cn(classes, className)} {...props}>
87+
<Link
88+
ref={innerRef}
89+
to={to}
90+
reloadDocument={reloadDocument}
91+
className={cn(classes, className)}
92+
{...props}
93+
>
7394
{linkContent}
7495
</Link>
7596
) : href ? (

apps/webapp/app/components/runs/v3/agent/AgentMessageView.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { UIMessage } from "@ai-sdk/react";
22
import { memo } from "react";
33
import { AssistantResponse, ChatBubble, ToolUseRow } from "~/components/runs/v3/ai/AIChatMessages";
44
import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover";
5+
import { textLinkClassName } from "~/components/primitives/TextLink";
56

67
// ---------------------------------------------------------------------------
78
// AgentMessageView — renders an AI SDK UIMessage[] conversation.
@@ -215,12 +216,7 @@ export function renderPart(part: UIMessage["parts"][number], i: number) {
215216
}
216217
return (
217218
<div key={i} className="text-xs">
218-
<a
219-
href={safeUrl}
220-
target="_blank"
221-
rel="noopener noreferrer"
222-
className="text-indigo-400 underline hover:text-indigo-300"
223-
>
219+
<a href={safeUrl} target="_blank" rel="noopener noreferrer" className={textLinkClassName()}>
224220
{label}
225221
</a>
226222
</div>
@@ -270,12 +266,7 @@ export function renderPart(part: UIMessage["parts"][number], i: number) {
270266
}
271267
return (
272268
<div key={i} className="text-xs">
273-
<a
274-
href={safeUrl}
275-
target="_blank"
276-
rel="noopener noreferrer"
277-
className="text-indigo-400 underline hover:text-indigo-300"
278-
>
269+
<a href={safeUrl} target="_blank" rel="noopener noreferrer" className={textLinkClassName()}>
279270
{p.filename ?? "Download file"}
280271
</a>
281272
</div>

apps/webapp/app/components/runs/v3/ai/AIToolsInventory.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { useState } from "react";
22
import { CodeBlock } from "~/components/code/CodeBlock";
33
import type { AISpanData, ToolDefinition } from "./types";
44
import { Paragraph } from "~/components/primitives/Paragraph";
5+
import { textLinkClassName } from "~/components/primitives/TextLink";
6+
import { cn } from "~/utils/cn";
57

68
export function AIToolsInventory({ aiData }: { aiData: AISpanData }) {
79
const defs = aiData.toolDefinitions ?? [];
@@ -48,7 +50,7 @@ function ToolDefRow({ def, wasCalled }: { def: ToolDefinition; wasCalled: boolea
4850
<div className="pl-3.5">
4951
<button
5052
onClick={() => setShowSchema(!showSchema)}
51-
className="text-[10px] text-text-link hover:underline"
53+
className={cn(textLinkClassName(), "text-[10px]")}
5254
>
5355
{showSchema ? "Hide schema" : "Show schema"}
5456
</button>

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Link, useLocation } from "@remix-run/react";
1+
import { useLocation } from "@remix-run/react";
22
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
33
import { typedjson, useTypedLoaderData } from "remix-typedjson";
44
import { useEffect, useState, useRef, useCallback } from "react";
@@ -53,6 +53,7 @@ import { capitalizeWord } from "~/utils/string";
5353
import { UserTag } from "../_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route";
5454
import { DeploymentEventFromString } from "@trigger.dev/core/v3/schemas";
5555
import { pageMeta } from "~/utils/pageTitle";
56+
import { TextLink } from "~/components/primitives/TextLink";
5657

5758
export const meta = pageMeta(({ params }) => [
5859
params.deploymentParam ?? "Deployment",
@@ -322,12 +323,12 @@ export default function Page() {
322323
<Property.Item>
323324
<Property.Label>Build Server</Property.Label>
324325
<Property.Value>
325-
<Link
326+
<TextLink
326327
to={`/resources/${deployment.projectId}/deployments/${deployment.id}/logs`}
327-
className="extra-small/bright/mono underline"
328+
className="font-mono text-xs"
328329
>
329330
{deployment.externalBuildData.buildId}
330-
</Link>
331+
</TextLink>
331332
</Property.Value>
332333
</Property.Item>
333334
)}

0 commit comments

Comments
 (0)