SPB Git

Repository Public MIT

sources.tsx 4.7 KB 91 lines tsx

1import { useState } from "react";2import { ExternalLink, Globe, ChevronDown } from "lucide-react";3import type { SearchResult } from "@shared/types";4import { hostnameOf } from "@/lib/format";5import { cn } from "@/lib/utils";67function Favicon({ src, url, className }: { src?: string; url: string; className?: string }) {8  const [failed, setFailed] = useState(false);9  const host = hostnameOf(url);10  if (src && !failed) {11    return <img src={src} alt="" className={cn("rounded-[5px] object-contain bg-white", className)} onError={() => setFailed(true)} loading="lazy" />;12  }13  return (14    <span className={cn("grid place-items-center rounded-[5px] bg-primary/10 text-primary font-mono text-[10px] font-semibold", className)}>15      {host[0]?.toUpperCase() ?? <Globe className="h-3 w-3" />}16    </span>17  );18}1920/** Rangée compacte de sources (au-dessus de la réponse). */21export function SourceChips({ sources, max = 5, className }: { sources: SearchResult[]; max?: number; className?: string }) {22  if (!sources?.length) return null;23  const shown = sources.slice(0, max);24  const rest = sources.length - shown.length;25  return (26    <div className={cn("flex flex-wrap items-center gap-1.5", className)} data-testid="source-chips">27      {shown.map((s, i) => (28        <a29          key={s.url + i}30          href={s.url}31          target="_blank"32          rel="noopener noreferrer"33          title={s.title}34          className="inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-2 py-1 text-[11.5px] text-muted-foreground hover:text-foreground hover:border-primary/40 transition-colors"35        >36          <Favicon src={s.favicon} url={s.url} className="h-3.5 w-3.5" />37          {hostnameOf(s.url)}38        </a>39      ))}40      {rest > 0 && <span className="text-[11.5px] text-muted-foreground">+{rest}</span>}41    </div>42  );43}4445/** Grille détaillée des sources (sous la réponse). */46export function SourcesGrid({ sources, className, defaultOpen = false }: { sources: SearchResult[]; className?: string; defaultOpen?: boolean }) {47  const [open, setOpen] = useState(defaultOpen);48  if (!sources?.length) return null;49  return (50    <section className={cn("surface overflow-hidden", className)} data-testid="web-results-section">51      <button type="button" onClick={() => setOpen((o) => !o)} className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-accent/50 transition-colors" aria-expanded={open}>52        <span className="grid h-8 w-8 place-items-center rounded-xl bg-brand-soft text-primary"><Globe className="h-4 w-4" /></span>53        <span className="flex-1">54          <span className="block text-sm font-semibold" data-testid="text-web-results-heading">Sources web</span>55          <span className="block text-[12px] text-muted-foreground">{sources.length} référence{sources.length > 1 ? "s" : ""} consultée{sources.length > 1 ? "s" : ""}</span>56        </span>57        <span className="hidden sm:flex -space-x-1.5">58          {sources.slice(0, 6).map((s, i) => <Favicon key={i} src={s.favicon} url={s.url} className="h-5 w-5 ring-2 ring-card" />)}59        </span>60        <ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform", open && "rotate-180")} />61      </button>62      {open && (63        <ol className="grid grid-cols-1 md:grid-cols-2 gap-2 border-t border-border p-3" data-testid="web-results-list">64          {sources.map((s, i) => (65            <li key={s.url + i}>66              <a67                href={s.url}68                target="_blank"69                rel="noopener noreferrer"70                className="group flex h-full items-start gap-3 rounded-xl border border-transparent bg-muted/40 p-3 hover:border-primary/30 hover:bg-card transition-colors"71                data-testid={`link-source-${i}`}72              >73                <Favicon src={s.favicon} url={s.url} className="mt-0.5 h-5 w-5" />74                <span className="min-w-0 flex-1">75                  <span className="flex items-center gap-1.5">76                    <span className="truncate text-[13px] font-medium text-foreground">{s.title || hostnameOf(s.url)}</span>77                    <ExternalLink className="h-3 w-3 shrink-0 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />78                  </span>79                  <span className="block text-[11px] text-muted-foreground font-mono">{hostnameOf(s.url)}</span>80                  {s.snippet && <span className="mt-1 block text-[12px] leading-snug text-muted-foreground line-clamp-2">{s.snippet}</span>}81                </span>82                <span className="shrink-0 grid h-5 w-5 place-items-center rounded-full bg-background text-[10px] font-semibold text-muted-foreground tnum">{i + 1}</span>83              </a>84            </li>85          ))}86        </ol>87      )}88    </section>89  );90}91
Files main
Menu

Theme