Ir al contenido
Evaluar mi proyecto
Componentes · IADisponible

Bloque de código para respuestas

Cabecera con nombre de archivo, números de línea, resaltado ligero sin librerías y copia con confirmación.

orden.ts
// Suma el total de una orden con impuestos
export function totalOrden(items: Item[], iva = 0.16) {
const subtotal = items.reduce((sum, item) => sum + item.precio * item.cantidad, 0);
return Math.round(subtotal * (1 + iva) * 100) / 100;
}

Código completo

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

ai-codigo.tsx109 líneas
Descargar
"use client";

import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";

/** Bloque de código para respuestas de un asistente: cabecera con lenguaje, números de línea, resaltado ligero y botón de copiar con confirmación. React + TypeScript, sin dependencias. Personaliza con --gcai-*. */
export type AiCodigoProps = {
  code?: string;
  language?: "ts" | "js" | "sql" | "json" | "bash";
  filename?: string;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcaicd-root { box-sizing: border-box; width: 100%; max-width: 620px; overflow: hidden; border: 1px solid var(--gcai-line, #26272c); border-radius: 12px; background: #0d0d0f; color: var(--gcai-ink, #ededee); font: 400 14px/1.6 ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
.gcaicd-root *, .gcaicd-root *::before, .gcaicd-root *::after { box-sizing: border-box; }
.gcaicd-root button { font: inherit; -webkit-tap-highlight-color: transparent; }
.gcaicd-root :focus-visible { outline: 2px solid var(--gcai-accent, #84b3ff); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gcaicd-root * { transition-duration: .01ms !important; } }
.gcaicd-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 8px 8px 16px; border-bottom: 1px solid var(--gcai-line, #26272c); background: var(--gcai-surface, #111113); font: 500 12px/1 ui-sans-serif, system-ui, sans-serif; color: var(--gcai-muted, #9b9ca3); }
.gcaicd-copy { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0 10px; border: 0; border-radius: 8px; background: transparent; color: inherit; cursor: pointer; transition: background .15s, color .15s; }
.gcaicd-copy:hover { background: var(--gcai-well, #1a1b1e); color: var(--gcai-ink, #ededee); }
.gcaicd-copy[data-done="true"] { color: #8fe3b0; }
.gcaicd-body { display: grid; grid-template-columns: auto minmax(0, 1fr); overflow-x: auto; padding: 14px 0; }
.gcaicd-nums { padding: 0 14px 0 16px; text-align: right; color: #4d4e56; user-select: none; }
.gcaicd-code { margin: 0; padding: 0 16px 0 0; white-space: pre; }
.gcaicd-code ::selection { background: rgba(132, 179, 255, .35); }
.gcaicd-k { color: #c3a6ff; } .gcaicd-s { color: #9fd9a8; } .gcaicd-c { color: #6f7079; font-style: italic; } .gcaicd-n { color: #f0b27a; } .gcaicd-f { color: #84b3ff; }`;

const KEYWORDS: Record<string, string> = {
  ts: "const let var function return if else for while import from export type interface async await new class extends true false null undefined",
  js: "const let var function return if else for while import from export async await new class extends true false null undefined",
  sql: "select from where group by order insert into values update set delete join left on and or not null as limit create table",
  json: "true false null",
  bash: "git npm npx cd ls echo export if then fi",
};

function highlight(line: string, lang: string): ReactNode[] {
  const kw = new Set(KEYWORDS[lang].split(" "));
  const re = /(\/\/.*|#.*|--.*)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`[^`]*`)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_][\w]*)(?=\()|([A-Za-z_][\w]*)/g;
  const out: ReactNode[] = [];
  let last = 0;
  let m: RegExpExecArray | null;
  while ((m = re.exec(line))) {
    if (m.index > last) out.push(line.slice(last, m.index));
    const [text, comment, str, num, fn, word] = m;
    if (comment) out.push(<span key={m.index} className="gcaicd-c">{text}</span>);
    else if (str) out.push(<span key={m.index} className="gcaicd-s">{text}</span>);
    else if (num) out.push(<span key={m.index} className="gcaicd-n">{text}</span>);
    else if (fn) out.push(kw.has(fn.toLowerCase()) ? <span key={m.index} className="gcaicd-k">{text}</span> : <span key={m.index} className="gcaicd-f">{text}</span>);
    else if (word && kw.has(lang === "sql" ? word.toLowerCase() : word)) out.push(<span key={m.index} className="gcaicd-k">{text}</span>);
    else out.push(text);
    last = m.index + text.length;
  }
  if (last < line.length) out.push(line.slice(last));
  return out;
}

const SAMPLE = `// Suma el total de una orden con impuestos
export function totalOrden(items: Item[], iva = 0.16) {
  const subtotal = items.reduce((sum, item) => sum + item.precio * item.cantidad, 0);
  return Math.round(subtotal * (1 + iva) * 100) / 100;
}`;

export function AiCodigo({ code = SAMPLE, language = "ts", filename = "orden.ts", className = "", style }: AiCodigoProps) {
  const [done, setDone] = useState(false);
  const timer = useRef(0);
  useEffect(() => () => window.clearTimeout(timer.current), []);
  const lines = code.split("\n");

  async function copy() {
    try {
      await navigator.clipboard.writeText(code);
      setDone(true);
      window.clearTimeout(timer.current);
      timer.current = window.setTimeout(() => setDone(false), 1800);
    } catch {
      setDone(false);
    }
  }

  return (
    <figure className={`gcaicd-root ${className}`} style={{ margin: 0, ...style }}>
      <style>{CSS}</style>
      <figcaption className="gcaicd-head">
        <span>{filename}</span>
        <button className="gcaicd-copy" type="button" data-done={done} onClick={copy} aria-label={done ? "Código copiado" : "Copiar código"}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">{done ? <path d="m5 12.5 4.5 4.5L19 7.5" /> : <><rect x="9" y="9" width="11" height="11" rx="2" /><path d="M5 15V6a2 2 0 0 1 2-2h8" /></>}</svg>
          {done ? "Copiado" : "Copiar"}
        </button>
      </figcaption>
      <div className="gcaicd-body">
        <div className="gcaicd-nums" aria-hidden="true">
          {lines.map((_, i) => (
            <div key={i}>{i + 1}</div>
          ))}
        </div>
        <pre className="gcaicd-code">
          <code>
            {lines.map((line, i) => (
              <div key={i}>{highlight(line, language) || " "}{line === "" ? " " : null}</div>
            ))}
          </code>
        </pre>
      </div>
    </figure>
  );
}

Ú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-codigo

Ejemplo de uso de Bloque de código para respuestas

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

ejemplo.tsx5 líneas
import { AiCodigo } from "./ai-codigo";

export default function Ejemplo() {
  return <AiCodigo language="ts" code="Ejemplo" />;
}

Propiedades de AiCodigo

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

PropiedadTipoDescripción
code?stringOpcional.
language?"ts" | "js" | "sql" | "json" | "bash"Opcional.
filename?stringOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Variantes disponibles

  • language: ts, js, sql, json, bash

Personaliza Bloque de código para respuestas 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
--gcai-line#26272c
--gcai-ink#ededee
--gcai-accent#84b3ff
--gcai-surface#111113
--gcai-muted#9b9ca3
--gcai-well#1a1b1e

Accesibilidad y rendimiento

El archivo tiene 109 líneas (6 KB) y no necesita @keyframes. Esto es lo que hace Bloque de código para respuestas 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)