"use client"
import { Mail } from 'lucide-react';
import { Button } from './ui/button';
import { GitHubIcon, RedditIcon, XLogo, LinkedInIcon, LumaIcon, WhatsAppIcon, GlobeIcon, MessageCircleIcon, TelegramIcon, YouTubeIcon, InstagramIcon, FacebookIcon, SlackIcon, CopyIcon } from './icons';
/** Exactly ONE of `href` (anchor, target _blank) or `onClick` (action
* button — share popups via window.open inside the click gesture,
* copy-to-clipboard) — the discriminated union makes a dead no-action
* entry unrepresentable. */
type SocialLink = {
platform: string;
label?: string;
/** `internal` links are only ever shown on surfaces that opt in (and must be
* gated server-side); with `groupByVisibility` they render after a divider. */
visibility?: "external" | "internal";
} & (
| { href: string; onClick?: never }
| { onClick: () => void; href?: never }
);
interface SocialIconRowProps {
className?: string;
links?: SocialLink[];
variant?: "accent" | "outline" | "transparent" | "destructive" | null | undefined;
/** Quiet metadata row for page-level identity/share slots: 32px ghost
* icon buttons (size="icon-sm", 16px glyphs), gap-2, w-fit container,
* variant defaulting to "transparent" (an explicit `variant` still wins).
* Default false: 44/48px buttons stretching across the container —
* the original card-width behavior (TMCG member cards, footers). */
compact?: boolean;
/** Render the `external` links and the `internal` links as two groups split
* by a thin divider (only when BOTH groups are non-empty). Visibility is
* presentation-only here — the caller must still gate which links it passes
* server-side. Default false keeps a single flat row. */
groupByVisibility?: boolean;
}
const defaultLinks: SocialLink[] = [
{ platform: 'github', href: 'https://github.com/flamingo-stack', label: 'GitHub' },
{ platform: 'linkedin', href: 'https://linkedin.com/company/flamingo.run', label: 'LinkedIn' },
{ platform: 'facebook', href: 'https://www.facebook.com/flamingoai.msp', label: 'Facebook' }
];
function renderSocialIcon(platform: string) {
const normalizedPlatform = platform.toLowerCase().trim();
switch (normalizedPlatform) {
case 'github':
return ;
case 'twitter':
case 'x':
return ;
case 'reddit':
return ;
case 'linkedin':
return ;
case 'luma':
return ;
case 'whatsapp':
return ;
case 'email':
case 'mail':
return ;
case 'website':
case 'web':
case 'url':
return ;
case 'slack':
return ;
case 'discord':
return ;
case 'telegram':
return ;
case 'youtube':
case 'yt':
return ;
case 'instagram':
case 'ig':
return ;
case 'facebook':
case 'fb':
return ;
case 'copy':
// CopyIcon's default fill is grey and would mismatch its row-mates —
// force the themed foreground via the ODS token (white on the dark
// theme, tracking the theme unlike the literal the reddit/slack cases
// still carry).
return ;
default:
return ;
}
}
export function SocialIconRow({ className = '', links = defaultLinks, variant, compact = false, groupByVisibility = false }: SocialIconRowProps) {
// ── Compact design rationale ──────────────────────────────────────────
// Page-level identity/share rows read as METADATA, not CTAs. The major
// design systems converge on one recipe for this slot: a ~32px ghost icon
// button with a ~16px glyph, tight 8px gap, transparent at rest, subtle
// background state-layer on hover (Carbon "ghost" sm, Primer "invisible"
// medium, shadcn ghost+icon-sm — all 32px; Material 3 "standard" icon
// button = state-layer hover). Author headers on content platforms
// (Medium / dev.to / Substack) use the same quiet treatment. Hence
// compact: size="icon-sm" + variant defaulting to "transparent" — the
// hover affordance comes from the bg state layer (hover:bg-ods-bg-hover
// inside the variant) because the brand icons carry fixed fills, not
// currentColor. An explicit `variant` prop still wins (e.g. outline
// chips). Non-compact keeps the legacy outline default + full-width
// stretch untouched.
const resolvedVariant = variant !== undefined ? variant : (compact ? 'transparent' : 'outline');
const renderButton = (link: SocialLink, index: number) => {
const ariaLabel = link.label || link.platform;
return link.onClick ? (
) : (
// Props-based linking — Button renders the anchor itself
// (openInNewTab carries target="_blank" + rel="noopener noreferrer");
// no asChild/ nesting.
);
};
const rowClass = `flex flex-row items-center ${compact ? 'gap-2 w-fit' : 'gap-3 w-full'} ${className}`;
// Two-group mode: external links, a thin divider, then internal links — but only
// when both groups are present (otherwise it's just a flat row, no stray divider).
if (groupByVisibility) {
const external = links.filter((l) => l.visibility !== 'internal');
const internal = links.filter((l) => l.visibility === 'internal');
if (external.length > 0 && internal.length > 0) {
return (