refactor: create reusable ExternalLink component for consistent styling

Create ExternalLink component following patterns from HighlightRenderer and
BookmarkRenderer with:
- Two variants: 'muted' (default, text-muted-foreground with underline)
  and 'default' (text-primary with hover:underline)
- Three sizes: xs, sm, base
- Configurable icon display
- Consistent truncate behavior for long URLs
- Stop propagation on click

Apply to NIP-89 renderers:
- ApplicationHandlerRenderer: uses muted variant (feed view)
- ApplicationHandlerDetailRenderer: uses default variant (detail view)

This ensures consistent link styling across the entire application
and makes it easy to maintain a unified design language.
This commit is contained in:
Claude
2026-01-05 09:51:43 +00:00
parent 91d801cbaf
commit f0ce89a7bb
3 changed files with 70 additions and 20 deletions

View File

@@ -0,0 +1,65 @@
import { ExternalLink as ExternalLinkIcon } from "lucide-react";
import { cn } from "@/lib/utils";
interface ExternalLinkProps {
href: string;
children: React.ReactNode;
className?: string;
iconClassName?: string;
showIcon?: boolean;
variant?: "default" | "muted";
size?: "xs" | "sm" | "base";
}
/**
* Reusable external link component with consistent styling across the app
* Follows patterns from HighlightRenderer and BookmarkRenderer
*/
export function ExternalLink({
href,
children,
className,
iconClassName,
showIcon = true,
variant = "muted",
size = "xs",
}: ExternalLinkProps) {
const sizeClasses = {
xs: "text-xs",
sm: "text-sm",
base: "text-base",
};
const iconSizeClasses = {
xs: "size-3",
sm: "size-3",
base: "size-4",
};
const variantClasses = {
default: "text-primary hover:underline",
muted: "text-muted-foreground underline decoration-dotted",
};
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={cn(
"inline-flex items-center gap-1",
sizeClasses[size],
variantClasses[variant],
className,
)}
onClick={(e) => e.stopPropagation()}
>
{showIcon && (
<ExternalLinkIcon
className={cn("flex-shrink-0", iconSizeClasses[size], iconClassName)}
/>
)}
<span className="truncate">{children}</span>
</a>
);
}

View File

@@ -11,13 +11,13 @@ import { KindBadge } from "@/components/KindBadge";
import { Badge } from "@/components/ui/badge";
import { useCopy } from "@/hooks/useCopy";
import { UserName } from "../UserName";
import { ExternalLink } from "@/components/ExternalLink";
import {
Copy,
CopyCheck,
Globe,
Smartphone,
TabletSmartphone,
ExternalLink,
} from "lucide-react";
interface ApplicationHandlerDetailRendererProps {
@@ -94,15 +94,9 @@ export function ApplicationHandlerDetailRenderer({
{/* Website */}
{website && (
<a
href={website}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-primary hover:underline"
>
<ExternalLink href={website} variant="default" size="base">
{website}
<ExternalLink className="size-3" />
</a>
</ExternalLink>
)}
{/* Metadata Grid */}

View File

@@ -11,6 +11,7 @@ import {
} from "@/lib/nip89-helpers";
import { KindBadge } from "@/components/KindBadge";
import { Badge } from "@/components/ui/badge";
import { ExternalLink } from "@/components/ExternalLink";
import { Globe, Smartphone, TabletSmartphone } from "lucide-react";
/**
@@ -60,17 +61,7 @@ export function ApplicationHandlerRenderer({ event }: BaseEventProps) {
</ClickableEventTitle>
{/* Website */}
{website && (
<a
href={website}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
{website}
</a>
)}
{website && <ExternalLink href={website}>{website}</ExternalLink>}
{/* Supported Kinds */}
{displayKinds.length > 0 && (