feat(desktop): style inline links as tinted chips

Content links read as a small primary-tinted chip instead of an
underline, dropping the trailing external-link arrow. The tint is
currentColor-relative, so one class carries text and fill in the same
hue across every theme, and `box-decoration-break: clone` gives a
wrapped link a chip per line fragment.
This commit is contained in:
Brooklyn Nicholson 2026-07-26 04:25:16 -05:00
parent 4dae897265
commit 6893faf472
6 changed files with 37 additions and 19 deletions

View file

@ -510,7 +510,7 @@ export const SessionRefLink: FC<{
return (
<a
className="font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere"
className="link-chip font-semibold wrap-anywhere"
href="#"
onClick={event => {
event.preventDefault()

View file

@ -100,7 +100,7 @@ function OpenMediaButton({ kind, path }: { kind: 'audio' | 'video'; path: string
return (
<span className="block">
<button
className="mt-2 bg-transparent text-xs font-medium text-muted-foreground underline underline-offset-4 decoration-current/20 hover:text-foreground"
className="mt-2 link-chip bg-transparent text-xs font-medium text-muted-foreground hover:text-foreground"
onClick={open}
type="button"
>
@ -196,7 +196,7 @@ function MediaAttachment({ path }: { path: string }) {
return (
<span className="wrap-anywhere">
<a
className="font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere"
className="link-chip font-semibold wrap-anywhere"
href="#"
onClick={event => {
event.preventDefault()
@ -246,10 +246,7 @@ function MarkdownLink({ children, className, href, ...props }: ComponentProps<'a
if (!target || !/^https?:\/\//i.test(target)) {
return (
<a
className={cn(
'font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere',
className
)}
className={cn('link-chip font-semibold wrap-anywhere', className)}
href={href}
rel="noopener noreferrer"
target="_blank"
@ -324,7 +321,7 @@ function MarkdownImage({ className, src, alt, ...props }: ComponentProps<'img'>)
<span className="my-2 block text-sm text-muted-foreground">
Couldn&apos;t load {name}.{' '}
<button
className="bg-transparent font-medium text-foreground underline underline-offset-4 decoration-current/20 hover:text-foreground"
className="link-chip bg-transparent font-medium text-foreground hover:text-foreground"
onClick={open}
type="button"
>

View file

@ -2,7 +2,7 @@ import type { ComponentProps, ElementType, FC } from 'react'
import { memo } from 'react'
import { Streamdown } from 'streamdown'
import { ExternalLink, ExternalLinkIcon } from '@/lib/external-link'
import { ExternalLink } from '@/lib/external-link'
import { cn } from '@/lib/utils'
// Compact markdown renderer for tool detail bodies. Same Streamdown pipeline
@ -42,20 +42,15 @@ function tagged<T extends keyof typeof TAG_CLASSES>(Tag: T) {
function MarkdownAnchor({ children, className, href, ...rest }: ComponentProps<'a'>) {
if (!href || !/^https?:\/\//i.test(href)) {
return (
<a
className={cn('font-medium underline underline-offset-4 decoration-current/20', className)}
href={href}
{...rest}
>
<a className={cn('link-chip font-medium', className)} href={href} {...rest}>
{children}
</a>
)
}
return (
<ExternalLink className={cn('decoration-current/20', className)} href={href} showExternalIcon={false}>
<ExternalLink className={className} href={href}>
{children}
<ExternalLinkIcon />
</ExternalLink>
)
}

View file

@ -96,7 +96,7 @@ export const GeneratedImage: FC<{ aspectRatio?: string; result?: unknown }> = ({
if (failed && image) {
return (
<a
className="mt-2 inline-block font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere"
className="mt-2 link-chip inline-block font-semibold wrap-anywhere"
href="#"
onClick={event => {
event.preventDefault()

View file

@ -215,14 +215,14 @@ export function ExternalLink({
className,
href,
onClick,
showExternalIcon = true,
showExternalIcon = false,
...rest
}: ExternalLinkProps) {
const target = normalizeExternalUrl(href)
return (
<a
className={cn('font-semibold text-foreground underline underline-offset-4 decoration-current/20', className)}
className={cn('link-chip font-semibold', className)}
href={target}
onClick={event => {
event.stopPropagation()

View file

@ -514,6 +514,32 @@
background: repeating-conic-gradient(currentColor 0% 25%, transparent 0% 50%) 0 0 / 0.125rem 0.125rem;
}
/* Inline content links: a small tinted chip instead of an underline. Tint is
currentColor-relative (the old `decoration-current/20` idiom), so text and
fill share a hue on every theme from one `color` declaration. */
.link-chip {
--link-chip-tint: 8%;
/* `ch`/`em` so the chip tracks the text at any size. Block padding is
asymmetric because an inline box's content area is ascent+descent equal
padding paints the glyphs high. Keep it small: `padding-block` doesn't grow
the line box, so an over-padded chip creeps into the line above. */
padding: 0.05ch 0.5ch 0.2ch;
border-radius: 0.25rem;
color: var(--dt-primary);
background: color-mix(in srgb, currentColor var(--link-chip-tint), transparent);
/* Explicit: the base layer underlines every `a` (see the :where(a, …) reset). */
text-decoration: none;
/* A wrapped link gets a chip per line fragment, not one ragged box. */
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
transition: background-color 0.12s ease;
}
.link-chip:hover {
--link-chip-tint: 15%;
}
/* Hover-reveal suppression the shared, declarative escape hatch.
A collapsed pane slides in when the pointer dwells on its thin edge trigger.
Controls that sit over that edge gutter would drag the panel in by accident.