Ir al contenido
Evaluar mi proyecto
Componentes · IADisponible

Lista de fuentes y citas

Fuentes numeradas con dominio, extracto y enlace externo seguro; expansión de listas largas y estado vacío.

Fuentes 4

  1. Documento del proyecto (abre una pestaña nueva)example.com

    Fuente de ejemplo: objetivos, alcance y entregables.

  2. Guía de implementación (abre una pestaña nueva)example.com

    Fuente de ejemplo: decisiones y próximos pasos.

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

ai-fuentes.tsx115 líneas
Descargar
"use client";

import { useState, useId, type CSSProperties } from "react";

/** Lista de fuentes y citas. React + TypeScript, sin dependencias. Personaliza con variables --gczs-*. */
export type AiSource = { title: string; url: string; excerpt: string };
export type AiFuentesProps = {
  sources?: AiSource[];
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gczs-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gczs-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gczs-root *, .gczs-root *::before, .gczs-root *::after { box-sizing: border-box; }
.gczs-root button, .gczs-root input, .gczs-root textarea, .gczs-root select { font: inherit; }
.gczs-root button, .gczs-root a { -webkit-tap-highlight-color: transparent; }
.gczs-root :focus-visible { outline: 2px solid var(--gczs-accent, #25724b); outline-offset: 4px; }
.gczs-root ::selection { background: var(--gczs-accent, #25724b); color: #fff; }
.gczs-root button:disabled { opacity: .5; cursor: not-allowed; }
.gczs-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gczs-root *, .gczs-root *::before, .gczs-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gczs-root { width:460px; }
.gczs-root h3 { margin:0 0 20px; font-size:18px; font-weight:600; display:flex; align-items:center; gap:10px; }
.gczs-root h3 span { border:1px solid #ced7d0; border-radius:4px; padding:1px 6px; font-size:11px; color:#56695a; }
.gczs-root ol { list-style:none; padding:0; margin:0; }
.gczs-root li { display:flex; gap:14px; padding:18px 0; border-top:1px solid #dbe1da; }
.gczs-number { color:#526657; font:11px/1.5 ui-monospace,monospace; width:20px; padding-top:2px; }
.gczs-root li>div { min-width:0; }
.gczs-root a { display:inline-flex; gap:8px; align-items:center; color:#244b35; font-size:14px; text-decoration:none; font-weight:550; overflow-wrap:anywhere; }
.gczs-root a:hover { text-decoration:underline; text-underline-offset:4px; }
.gczs-root small { display:block; color:#667568; font-size:11px; margin-top:4px; overflow-wrap:anywhere; }
.gczs-root p { color:#556459; font-size:12px; margin:10px 0 0; }
.gczs-root button { padding:10px 14px; min-height:44px; background:transparent; color:#244b35; border:1px solid #b6c6b9; border-radius:5px; margin-top:16px; cursor:pointer; font-size:12px; }
.gczs-sr { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }`;

export function AiFuentes({
  sources = [
    {
      title: "Documento del proyecto",
      url: "https://example.com/documento",
      excerpt: "Fuente de ejemplo: objetivos, alcance y entregables.",
    },
    {
      title: "Guía de implementación",
      url: "https://example.com/guia",
      excerpt: "Fuente de ejemplo: decisiones y próximos pasos.",
    },
  ],
  className = "",
  style,
}: AiFuentesProps) {
  const [expanded, setExpanded] = useState(false);
  const id = useId();
  const shown = expanded ? sources : sources.slice(0, 2);
  function domain(url: string) {
    try {
      return new URL(url).hostname;
    } catch {
      return "Fuente";
    }
  }
  return (
    <section
      className={`gczs-root ${className}`}
      style={style}
      aria-labelledby={`${id}-title`}
    >
      <style>{CSS}</style>
      <h3 id={`${id}-title`}>
        Fuentes <span>{sources.length}</span>
      </h3>
      <ol id={`${id}-list`}>
        {shown.map((source, i) => (
          <li key={`${source.url}-${i}`}>
            <span className="gczs-number" aria-hidden="true">
              {i + 1}
            </span>
            <div>
              <a href={source.url} target="_blank" rel="noopener noreferrer">
                {source.title}
                <svg
                  width="14"
                  height="14"
                  viewBox="0 0 20 20"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="1.5"
                  aria-hidden="true"
                >
                  <path d="M5 15 15 5M5 5h10v10" />
                </svg>
                <span className="gczs-sr"> (abre una pestaña nueva)</span>
              </a>
              <small>{domain(source.url)}</small>
              <p>{source.excerpt}</p>
            </div>
          </li>
        ))}
      </ol>
      {!sources.length && <p>No se adjuntaron fuentes.</p>}
      {sources.length > 2 && (
        <button
          type="button"
          aria-expanded={expanded}
          aria-controls={`${id}-list`}
          onClick={() => setExpanded(!expanded)}
        >
          {expanded ? "Mostrar menos" : `Ver las ${sources.length} fuentes`}
        </button>
      )}
    </section>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/ai-fuentes

Ejemplo de uso de Lista de fuentes y citas

Después de copiar ai-fuentes.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { AiFuentes } from "./ai-fuentes";

export default function Ejemplo() {
  return <AiFuentes />;
}

Propiedades de AiFuentes

AiFuentes acepta 3 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
sources?AiSource[]Opcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Lista de fuentes y citas con variables CSS

Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.

VariableValor por defecto
--gczs-fg#202421
--gczs-accent#25724b

Accesibilidad y rendimiento

El archivo tiene 115 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Lista de fuentes y citas por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
  • Expone su estado a tecnologías de apoyo con atributos aria-*.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)