From 0d19e93f5da13d425606286f80a47d07960d3586 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Jul 2026 21:23:46 -0500 Subject: [PATCH 1/3] feat(desktop): resolve a hostname to its Simple Icons brand mark A lookup table of ~170 hosts plus a resolver that walks the hostname's suffixes, so subdomains inherit their parent's mark (gist.github.com) while a more specific entry (docs.google.com) still wins over the general one. --- apps/desktop/src/lib/brand-icon.test.ts | 43 +++ apps/desktop/src/lib/brand-icon.ts | 426 ++++++++++++++++++++++++ 2 files changed, 469 insertions(+) create mode 100644 apps/desktop/src/lib/brand-icon.test.ts create mode 100644 apps/desktop/src/lib/brand-icon.ts diff --git a/apps/desktop/src/lib/brand-icon.test.ts b/apps/desktop/src/lib/brand-icon.test.ts new file mode 100644 index 000000000000..1b23ec458587 --- /dev/null +++ b/apps/desktop/src/lib/brand-icon.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' + +import { resolveBrandIcon } from './brand-icon' + +describe('resolveBrandIcon', () => { + it('resolves a registrable domain to a brand glyph', () => { + expect(resolveBrandIcon('github.com')).toBeTruthy() + expect(resolveBrandIcon('gitlab.com')).toBeTruthy() + }) + + it('ignores case and a leading www.', () => { + const github = resolveBrandIcon('github.com') + + expect(resolveBrandIcon('WWW.GitHub.com')).toBe(github) + }) + + it('inherits the parent brand on subdomains', () => { + const github = resolveBrandIcon('github.com') + + expect(resolveBrandIcon('gist.github.com')).toBe(github) + expect(resolveBrandIcon('api.github.com')).toBe(github) + }) + + it('prefers the longest matching suffix over its parent', () => { + const google = resolveBrandIcon('google.com') + const docs = resolveBrandIcon('docs.google.com') + + expect(docs).toBeTruthy() + expect(docs).not.toBe(google) + }) + + it('returns null for unknown hosts and bare labels', () => { + expect(resolveBrandIcon('example.com')).toBeNull() + expect(resolveBrandIcon('localhost')).toBeNull() + expect(resolveBrandIcon('')).toBeNull() + }) + + it('never matches on a bare public suffix', () => { + // `github.io` is a real entry; a lone `io` must not inherit it. + expect(resolveBrandIcon('io')).toBeNull() + expect(resolveBrandIcon('com')).toBeNull() + }) +}) diff --git a/apps/desktop/src/lib/brand-icon.ts b/apps/desktop/src/lib/brand-icon.ts new file mode 100644 index 000000000000..000a066d858f --- /dev/null +++ b/apps/desktop/src/lib/brand-icon.ts @@ -0,0 +1,426 @@ +import { + SiAnthropic, + SiArchlinux, + SiArxiv, + SiAsana, + SiAtlassian, + SiBehance, + SiBitbucket, + SiBlender, + SiBluesky, + SiBun, + SiBuymeacoffee, + SiCircleci, + SiClaude, + SiCloudflare, + SiCodeberg, + SiCodecov, + SiCodesandbox, + SiConfluence, + SiCoursera, + SiCrunchbase, + SiCursor, + SiDatadog, + SiDebian, + SiDeno, + SiDevdotto, + SiDigitalocean, + SiDiscord, + SiDjango, + SiDocker, + SiDropbox, + SiElectron, + SiEslint, + SiExcalidraw, + SiFacebook, + SiFastapi, + SiFigma, + SiFirebase, + SiFlask, + SiForgejo, + SiGhost, + SiGitea, + SiGithub, + SiGitlab, + SiGmail, + SiGo, + SiGodotengine, + SiGoodreads, + SiGoogle, + SiGoogledocs, + SiGoogledrive, + SiGooglegemini, + SiGooglemaps, + SiGooglescholar, + SiGrafana, + SiGraphql, + SiHashnode, + SiHomebrew, + SiHuggingface, + SiImdb, + SiInstagram, + SiInternetarchive, + SiItchdotio, + SiJenkins, + SiJira, + SiJupyter, + SiKaggle, + SiKofi, + SiKotlin, + SiKubernetes, + SiLaravel, + SiLeetcode, + SiLinear, + SiMastodon, + SiMdnwebdocs, + SiMedium, + SiMiro, + SiMistralai, + SiMongodb, + SiNetflix, + SiNetlify, + SiNodedotjs, + SiNotion, + SiNpm, + SiNvidia, + SiObsidian, + SiOllama, + SiOpenrouter, + SiOpenstreetmap, + SiOverleaf, + SiPatreon, + SiPaypal, + SiPerplexity, + SiPhp, + SiPinterest, + SiPnpm, + SiPostgresql, + SiPostman, + SiPrisma, + SiProducthunt, + SiPypi, + SiPython, + SiPytorch, + SiQuora, + SiRailway, + SiRaspberrypi, + SiRaycast, + SiReact, + SiReadthedocs, + SiReddit, + SiRedis, + SiRender, + SiReplicate, + SiReplit, + SiResearchgate, + SiRuby, + SiRubyonrails, + SiRust, + SiScikitlearn, + SiSemanticscholar, + SiSentry, + SiServerfault, + SiShopify, + SiSnyk, + SiSoundcloud, + SiSourcehut, + SiSpotify, + SiStackblitz, + SiStackexchange, + SiStackoverflow, + SiSteam, + SiStorybook, + SiStripe, + SiSubstack, + SiSupabase, + SiSuperuser, + SiSwift, + SiTailwindcss, + SiTauri, + SiTelegram, + SiTensorflow, + SiThreedotjs, + SiTiktok, + SiTldraw, + SiTrello, + SiTurborepo, + SiTwitch, + SiTypescript, + SiUbuntu, + SiUdemy, + SiUnity, + SiUnsplash, + SiVercel, + SiVimeo, + SiVite, + SiWarp, + SiWebflow, + SiWeightsandbiases, + SiWikipedia, + SiWindsurf, + SiWordpress, + SiX, + SiYcombinator, + SiYelp, + SiYoutube, + SiZedindustries, + SiZoom, + SiZotero +} from '@icons-pack/react-simple-icons' +import type { ComponentType, SVGProps } from 'react' + +// Simple Icons components accept a `title` prop on top of the usual SVG props. +// It matters: they always render a element, defaulting to the brand +// name, so callers need to be able to blank it out (see `LinkBrandIcon`). +export type BrandIcon = ComponentType<SVGProps<SVGSVGElement> & { title?: string }> + +// simpleicons.org brand marks keyed by registrable domain. Lookup walks the +// host's suffixes (see `resolveBrandIcon`), so one `github.com` entry also +// covers `gist.github.com` and `api.github.com` — only list a subdomain when it +// belongs to a *different* brand than its parent. +// +// Some brands are absent on purpose: Simple Icons has removed Slack, LinkedIn, +// OpenAI, Amazon, and CodePen at their owners' request, so those hosts fall +// through to no glyph rather than a lookalike. +const BRAND_ICONS: Record<string, BrandIcon> = { + // Code hosting & package registries + 'github.com': SiGithub, + 'github.io': SiGithub, + 'githubusercontent.com': SiGithub, + 'gitlab.com': SiGitlab, + 'gitlab.io': SiGitlab, + 'bitbucket.org': SiBitbucket, + 'codeberg.org': SiCodeberg, + 'gitea.com': SiGitea, + 'gitea.io': SiGitea, + 'forgejo.org': SiForgejo, + 'sr.ht': SiSourcehut, + 'npmjs.com': SiNpm, + 'pypi.org': SiPypi, + 'crates.io': SiRust, + 'huggingface.co': SiHuggingface, + 'hf.co': SiHuggingface, + + // Q&A, docs & reference + 'stackoverflow.com': SiStackoverflow, + 'stackexchange.com': SiStackexchange, + 'serverfault.com': SiServerfault, + 'superuser.com': SiSuperuser, + 'developer.mozilla.org': SiMdnwebdocs, + 'readthedocs.io': SiReadthedocs, + 'readthedocs.org': SiReadthedocs, + 'wikipedia.org': SiWikipedia, + 'archive.org': SiInternetarchive, + + // Research + 'arxiv.org': SiArxiv, + 'semanticscholar.org': SiSemanticscholar, + 'scholar.google.com': SiGooglescholar, + 'researchgate.net': SiResearchgate, + 'zotero.org': SiZotero, + 'overleaf.com': SiOverleaf, + + // AI + 'anthropic.com': SiAnthropic, + 'claude.ai': SiClaude, + 'gemini.google.com': SiGooglegemini, + 'perplexity.ai': SiPerplexity, + 'openrouter.ai': SiOpenrouter, + 'mistral.ai': SiMistralai, + 'ollama.com': SiOllama, + 'replicate.com': SiReplicate, + 'wandb.ai': SiWeightsandbiases, + 'kaggle.com': SiKaggle, + + // Social + 'x.com': SiX, + 'twitter.com': SiX, + 't.co': SiX, + 'reddit.com': SiReddit, + 'redd.it': SiReddit, + 'news.ycombinator.com': SiYcombinator, + 'bsky.app': SiBluesky, + 'mastodon.social': SiMastodon, + 'joinmastodon.org': SiMastodon, + 'facebook.com': SiFacebook, + 'instagram.com': SiInstagram, + 'tiktok.com': SiTiktok, + 'pinterest.com': SiPinterest, + 'discord.com': SiDiscord, + 'discord.gg': SiDiscord, + 't.me': SiTelegram, + 'telegram.org': SiTelegram, + 'producthunt.com': SiProducthunt, + 'crunchbase.com': SiCrunchbase, + 'quora.com': SiQuora, + + // Writing & blogs + 'medium.com': SiMedium, + 'substack.com': SiSubstack, + 'dev.to': SiDevdotto, + 'hashnode.dev': SiHashnode, + 'hashnode.com': SiHashnode, + 'wordpress.com': SiWordpress, + 'wordpress.org': SiWordpress, + 'ghost.org': SiGhost, + + // Media + 'youtube.com': SiYoutube, + 'youtu.be': SiYoutube, + 'vimeo.com': SiVimeo, + 'twitch.tv': SiTwitch, + 'soundcloud.com': SiSoundcloud, + 'spotify.com': SiSpotify, + 'netflix.com': SiNetflix, + 'imdb.com': SiImdb, + 'goodreads.com': SiGoodreads, + 'unsplash.com': SiUnsplash, + 'behance.net': SiBehance, + 'store.steampowered.com': SiSteam, + 'itch.io': SiItchdotio, + + // Product & project tools + 'linear.app': SiLinear, + 'notion.so': SiNotion, + 'notion.site': SiNotion, + 'figma.com': SiFigma, + 'atlassian.net': SiAtlassian, + 'atlassian.com': SiJira, + 'confluence.com': SiConfluence, + 'asana.com': SiAsana, + 'trello.com': SiTrello, + 'obsidian.md': SiObsidian, + 'miro.com': SiMiro, + 'excalidraw.com': SiExcalidraw, + 'tldraw.com': SiTldraw, + 'zoom.us': SiZoom, + + // Google + 'google.com': SiGoogle, + 'goo.gl': SiGoogle, + 'docs.google.com': SiGoogledocs, + 'drive.google.com': SiGoogledrive, + 'mail.google.com': SiGmail, + 'maps.google.com': SiGooglemaps, + 'openstreetmap.org': SiOpenstreetmap, + + // Hosting & infra + 'vercel.com': SiVercel, + 'vercel.app': SiVercel, + 'netlify.com': SiNetlify, + 'netlify.app': SiNetlify, + 'cloudflare.com': SiCloudflare, + 'pages.dev': SiCloudflare, + 'workers.dev': SiCloudflare, + 'railway.app': SiRailway, + 'render.com': SiRender, + 'digitalocean.com': SiDigitalocean, + 'firebase.google.com': SiFirebase, + 'supabase.com': SiSupabase, + 'docker.com': SiDocker, + 'kubernetes.io': SiKubernetes, + 'grafana.com': SiGrafana, + 'datadoghq.com': SiDatadog, + 'sentry.io': SiSentry, + 'snyk.io': SiSnyk, + 'codecov.io': SiCodecov, + 'circleci.com': SiCircleci, + 'jenkins.io': SiJenkins, + + // Languages, frameworks & tooling + 'python.org': SiPython, + 'nodejs.org': SiNodedotjs, + 'react.dev': SiReact, + 'typescriptlang.org': SiTypescript, + 'tailwindcss.com': SiTailwindcss, + 'vite.dev': SiVite, + 'vitejs.dev': SiVite, + 'rust-lang.org': SiRust, + 'go.dev': SiGo, + 'golang.org': SiGo, + 'ruby-lang.org': SiRuby, + 'rubyonrails.org': SiRubyonrails, + 'php.net': SiPhp, + 'laravel.com': SiLaravel, + 'djangoproject.com': SiDjango, + 'flask.palletsprojects.com': SiFlask, + 'fastapi.tiangolo.com': SiFastapi, + 'swift.org': SiSwift, + 'kotlinlang.org': SiKotlin, + 'deno.com': SiDeno, + 'bun.sh': SiBun, + 'pnpm.io': SiPnpm, + 'turborepo.com': SiTurborepo, + 'eslint.org': SiEslint, + 'storybook.js.org': SiStorybook, + 'graphql.org': SiGraphql, + 'prisma.io': SiPrisma, + 'postgresql.org': SiPostgresql, + 'redis.io': SiRedis, + 'mongodb.com': SiMongodb, + 'pytorch.org': SiPytorch, + 'tensorflow.org': SiTensorflow, + 'scikit-learn.org': SiScikitlearn, + 'jupyter.org': SiJupyter, + 'threejs.org': SiThreedotjs, + 'blender.org': SiBlender, + 'godotengine.org': SiGodotengine, + 'unity.com': SiUnity, + 'electronjs.org': SiElectron, + 'tauri.app': SiTauri, + 'brew.sh': SiHomebrew, + 'archlinux.org': SiArchlinux, + 'debian.org': SiDebian, + 'ubuntu.com': SiUbuntu, + 'raspberrypi.com': SiRaspberrypi, + 'nvidia.com': SiNvidia, + + // Dev environments & editors + 'codesandbox.io': SiCodesandbox, + 'stackblitz.com': SiStackblitz, + 'replit.com': SiReplit, + 'cursor.com': SiCursor, + 'windsurf.com': SiWindsurf, + 'zed.dev': SiZedindustries, + 'warp.dev': SiWarp, + 'raycast.com': SiRaycast, + 'postman.com': SiPostman, + + // Commerce, learning & the rest + 'stripe.com': SiStripe, + 'paypal.com': SiPaypal, + 'patreon.com': SiPatreon, + 'ko-fi.com': SiKofi, + 'buymeacoffee.com': SiBuymeacoffee, + 'shopify.com': SiShopify, + 'webflow.com': SiWebflow, + 'dropbox.com': SiDropbox, + 'leetcode.com': SiLeetcode, + 'coursera.org': SiCoursera, + 'udemy.com': SiUdemy, + 'yelp.com': SiYelp +} + +// Resolve a hostname to its brand glyph, walking suffixes so subdomains inherit +// their parent's mark (`api.github.com` → `github.com`). Longest match wins, so +// a subdomain entry like `docs.google.com` beats the `google.com` fallback. The +// bare TLD is never tested. +export function resolveBrandIcon(hostname: string): BrandIcon | null { + const host = hostname.trim().toLowerCase().replace(/^www\./, '') + + if (!host.includes('.')) { + return null + } + + const parts = host.split('.') + + for (let i = 0; i < parts.length - 1; i += 1) { + const icon = BRAND_ICONS[parts.slice(i).join('.')] + + if (icon) { + return icon + } + } + + return null +} From 23459c036138a5817e1770e120df27d48eb49a87 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com> Date: Mon, 27 Jul 2026 21:23:46 -0500 Subject: [PATCH 2/3] feat(desktop): show brand icons on links to known domains PrettyLink leads with the site's mark, so a GitHub PR link reads as a GitHub link at a glance. The artifacts pane uses the same lookup in place of its generic chain glyph; unknown hosts render as before. --- apps/desktop/src/app/artifacts/index.tsx | 13 +++++++++-- apps/desktop/src/lib/external-link.test.tsx | 24 +++++++++++++++++++++ apps/desktop/src/lib/external-link.tsx | 18 ++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/app/artifacts/index.tsx b/apps/desktop/src/app/artifacts/index.tsx index 70a6a0338ca3..bdcf5d103e2c 100644 --- a/apps/desktop/src/app/artifacts/index.tsx +++ b/apps/desktop/src/app/artifacts/index.tsx @@ -19,7 +19,15 @@ import { RowButton } from '@/components/ui/row-button' import { Tip } from '@/components/ui/tooltip' import { getSessionMessages, listAllProfileSessions } from '@/hermes' import { type Translations, useI18n } from '@/i18n' -import { ExternalLink, ExternalLinkIcon, hostPathLabel, urlSlugTitleLabel, useLinkTitle } from '@/lib/external-link' +import { resolveBrandIcon } from '@/lib/brand-icon' +import { + ExternalLink, + ExternalLinkIcon, + hostPathLabel, + shortHostLabel, + urlSlugTitleLabel, + useLinkTitle +} from '@/lib/external-link' import { FileImage, FileText, FolderOpen, Link2, Loader2, RefreshCw } from '@/lib/icons' import { downloadGatewayMediaFile, isRemoteGateway } from '@/lib/media' import { normalize } from '@/lib/text' @@ -543,7 +551,8 @@ function ArtifactCellAction({ function PrimaryCell({ artifact, ctx }: { artifact: ArtifactRecord; ctx: CellCtx }) { const isLink = artifact.kind === 'link' - const Icon = isLink ? Link2 : FileText + const brand = isLink ? resolveBrandIcon(shortHostLabel(artifact.href)) : null + const Icon = brand ?? (isLink ? Link2 : FileText) const fetchedTitle = useLinkTitle(isLink ? artifact.href : null) const label = isLink ? fetchedTitle || urlSlugTitleLabel(artifact.href) : artifact.label diff --git a/apps/desktop/src/lib/external-link.test.tsx b/apps/desktop/src/lib/external-link.test.tsx index 57c0176e37c8..5407639199c4 100644 --- a/apps/desktop/src/lib/external-link.test.tsx +++ b/apps/desktop/src/lib/external-link.test.tsx @@ -248,4 +248,28 @@ describe('external link helpers', () => { const link = screen.getByRole('link', { name: 'agent.log' }) expect(link.getAttribute('href')).toBe('https://agent.log') }) + + it('prefixes a pretty link to a known host with its brand glyph', () => { + installDesktopBridge() + + const url = 'https://github.com/NousResearch/hermes-agent/pull/123' + + render(<PrettyLink fallbackLabel="#123" href={url} />) + + const link = screen.getByTitle(url) + + expect(link.querySelector('svg')).toBeTruthy() + // The glyph is decorative — it must not pollute the link's accessible name. + expect(link.textContent).toBe('#123') + }) + + it('renders no brand glyph for an unknown host', () => { + installDesktopBridge() + + const url = 'https://example.com/some/page' + + render(<PrettyLink fallbackLabel="Some Page" href={url} />) + + expect(screen.getByTitle(url).querySelector('svg')).toBeNull() + }) }) diff --git a/apps/desktop/src/lib/external-link.tsx b/apps/desktop/src/lib/external-link.tsx index 17f82c6a2cb9..4f6dc396b7d6 100644 --- a/apps/desktop/src/lib/external-link.tsx +++ b/apps/desktop/src/lib/external-link.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react' import { ArrowUpRight } from '@/lib/icons' +import { resolveBrandIcon } from './brand-icon' import { cn } from './utils' const titleCache = new Map<string, string>() @@ -210,6 +211,22 @@ export function ExternalLinkIcon({ className }: { className?: string }) { return <ArrowUpRight aria-hidden className={cn('ml-1 inline size-[0.78em] align-[-0.08em] opacity-70', className)} /> } +// Brand mark for a known host, sized in `em` so it tracks the surrounding text +// at any font size. It paints in `currentColor` rather than the brand hex — +// several brand colors (GitHub's near-black, Unity's white) vanish against one +// theme or the other. +// +// `title=""` is load-bearing: Simple Icons always renders a <title> defaulting +// to the brand name, which lands in the anchor's textContent and accessible +// name — a PR link would read "GitHub#123". +export function LinkBrandIcon({ className, href }: { className?: string; href: string }) { + const Icon = resolveBrandIcon(shortHostLabel(href)) + + return Icon ? ( + <Icon aria-hidden className={cn('mr-1 inline size-[0.85em] align-[-0.12em] opacity-80', className)} title="" /> + ) : null +} + export function ExternalLink({ children, className, @@ -261,6 +278,7 @@ export function PrettyLink({ className, fallbackLabel, href, label, ...rest }: P return ( <ExternalLink className={cn('wrap-break-word', className)} href={target} title={target} {...rest}> + <LinkBrandIcon href={target} /> <span className="font-medium">{display}</span> </ExternalLink> ) From df7352aec657abd988e98897b8bb194d808de044 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com> Date: Mon, 27 Jul 2026 21:45:42 -0500 Subject: [PATCH 3/3] style(desktop): quiet inline links to color-only until hover The resting chip fill and the bold weight both fought the brand mark for attention. Links now sit in the theme's primary color at body weight, and the tint fades in on hover. Weight is overridden on .link-chip itself: `@tailwindcss/typography` sets `prose a { font-weight: 500 }`, which outranks a utility class on the anchor, so the per-call-site classes could never win. --- .../src/components/assistant-ui/directive-text.tsx | 2 +- .../src/components/assistant-ui/markdown-text.tsx | 4 ++-- .../src/components/chat/compact-markdown.tsx | 2 +- .../src/components/chat/generated-image-result.tsx | 2 +- apps/desktop/src/lib/external-link.tsx | 4 ++-- apps/desktop/src/styles.css | 14 ++++++++++---- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/directive-text.tsx b/apps/desktop/src/components/assistant-ui/directive-text.tsx index 988562c04941..b8e8ce66eca6 100644 --- a/apps/desktop/src/components/assistant-ui/directive-text.tsx +++ b/apps/desktop/src/components/assistant-ui/directive-text.tsx @@ -521,7 +521,7 @@ export const SessionRefLink: FC<{ return ( <a - className="link-chip font-semibold wrap-anywhere" + className="link-chip wrap-anywhere" href="#" onClick={event => { event.preventDefault() diff --git a/apps/desktop/src/components/assistant-ui/markdown-text.tsx b/apps/desktop/src/components/assistant-ui/markdown-text.tsx index 3d9b44bf9ee9..744bdb61c1b2 100644 --- a/apps/desktop/src/components/assistant-ui/markdown-text.tsx +++ b/apps/desktop/src/components/assistant-ui/markdown-text.tsx @@ -233,7 +233,7 @@ function MediaAttachment({ path }: { path: string }) { return ( <span className="wrap-anywhere"> <a - className="link-chip font-semibold wrap-anywhere" + className="link-chip wrap-anywhere" href="#" onClick={event => { event.preventDefault() @@ -283,7 +283,7 @@ function MarkdownLink({ children, className, href, ...props }: ComponentProps<'a if (!target || !/^https?:\/\//i.test(target)) { return ( <a - className={cn('link-chip font-semibold wrap-anywhere', className)} + className={cn('link-chip wrap-anywhere', className)} href={href} rel="noopener noreferrer" target="_blank" diff --git a/apps/desktop/src/components/chat/compact-markdown.tsx b/apps/desktop/src/components/chat/compact-markdown.tsx index eedd6ad18ef7..8426be108a56 100644 --- a/apps/desktop/src/components/chat/compact-markdown.tsx +++ b/apps/desktop/src/components/chat/compact-markdown.tsx @@ -42,7 +42,7 @@ 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('link-chip font-medium', className)} href={href} {...rest}> + <a className={cn('link-chip', className)} href={href} {...rest}> {children} </a> ) diff --git a/apps/desktop/src/components/chat/generated-image-result.tsx b/apps/desktop/src/components/chat/generated-image-result.tsx index 32cb453c69c6..9ad4922644e3 100644 --- a/apps/desktop/src/components/chat/generated-image-result.tsx +++ b/apps/desktop/src/components/chat/generated-image-result.tsx @@ -96,7 +96,7 @@ export const GeneratedImage: FC<{ aspectRatio?: string; result?: unknown }> = ({ if (failed && image) { return ( <a - className="mt-2 link-chip inline-block font-semibold wrap-anywhere" + className="mt-2 link-chip inline-block wrap-anywhere" href="#" onClick={event => { event.preventDefault() diff --git a/apps/desktop/src/lib/external-link.tsx b/apps/desktop/src/lib/external-link.tsx index 4f6dc396b7d6..df853911247a 100644 --- a/apps/desktop/src/lib/external-link.tsx +++ b/apps/desktop/src/lib/external-link.tsx @@ -239,7 +239,7 @@ export function ExternalLink({ return ( <a - className={cn('link-chip font-semibold', className)} + className={cn('link-chip', className)} href={target} onClick={event => { event.stopPropagation() @@ -279,7 +279,7 @@ export function PrettyLink({ className, fallbackLabel, href, label, ...rest }: P return ( <ExternalLink className={cn('wrap-break-word', className)} href={target} title={target} {...rest}> <LinkBrandIcon href={target} /> - <span className="font-medium">{display}</span> + {display} </ExternalLink> ) } diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index 541da35b6bc3..60b231ffaf60 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -556,11 +556,17 @@ 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. */ +/* Inline content links: color at rest, a tinted chip on hover, never 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`. */ .link-chip { - --link-chip-tint: 8%; + /* The one knob: resting fill. 0% = color-only until hovered. */ + --link-chip-tint: 0%; + + /* Prose links inherit the surrounding text's weight. `@tailwindcss/typography` + sets `prose a { font-weight: 500 }`, which outranks a utility class on the + anchor — so the override belongs here, on the shared chip. */ + font-weight: inherit; /* `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