Ir al contenido
Evaluar mi proyecto
Componentes · FootersDisponible

Footer de terminal con árbol del sitio

Mapa del sitio como árbol de directorios con enlaces reales, prompt con cursor parpadeante y estado del sistema.

visitante@sitio:~$ tree --mapa-del-sitio

visitante@sitio:~$

todos los sistemas operativos© 2026 · hecho con cariño y café

Código completo

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

footer-terminal.tsx81 líneas
Descargar
"use client";

import type { CSSProperties } from "react";

/** Footer de terminal: el mapa del sitio como un árbol de directorios con enlaces reales, prompt con cursor y estado del sistema. React + TypeScript, sin dependencias. */
export type FooterTerminalGrupo = { dir: string; links: { label: string; href?: string }[] };

export type FooterTerminalProps = {
  user?: string;
  groups?: FooterTerminalGrupo[];
  status?: string;
  legal?: string;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcfte-root { box-sizing: border-box; container-type: inline-size; width: 100%; padding: clamp(22px, 4cqi, 48px); background: #07090b; color: #8be8a6; font: 500 14px/1.7 ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
.gcfte-root *, .gcfte-root *::before, .gcfte-root *::after { box-sizing: border-box; }
.gcfte-root :focus-visible { outline: 1px dashed #8be8a6; outline-offset: 3px; }
.gcfte-root ::selection { background: #8be8a6; color: #07090b; }
@media (prefers-reduced-motion: reduce) { .gcfte-root *, .gcfte-root *::before, .gcfte-root *::after { animation: none !important; } }
.gcfte-prompt { margin: 0 0 14px; color: #c9f7d6; }
.gcfte-prompt b { color: #62b4ff; font-weight: 500; }
.gcfte-caret { display: inline-block; width: .6em; height: 1.1em; margin-left: 2px; vertical-align: text-bottom; background: #8be8a6; animation: gcfte-blink 1s steps(1) infinite; }
.gcfte-tree { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 18px 32px; margin-bottom: 22px; }
.gcfte-dir { margin: 0; color: #62b4ff; }
.gcfte-list { margin: 0; padding: 0; list-style: none; }
.gcfte-list li { position: relative; padding-left: 2.2ch; color: #4f8a63; }
.gcfte-list li::before { content: "├─"; position: absolute; left: 0; }
.gcfte-list li:last-child::before { content: "└─"; }
.gcfte-list a { color: #8be8a6; text-decoration: none; }
.gcfte-list a:hover { background: #8be8a6; color: #07090b; }
.gcfte-foot { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6px 24px; padding-top: 14px; border-top: 1px dashed #1e3a2a; font-size: 13px; color: #4f8a63; }
.gcfte-dot { display: inline-block; width: 8px; height: 8px; margin-right: 8px; border-radius: 50%; background: #8be8a6; box-shadow: 0 0 8px #8be8a6; animation: gcfte-pulse 2s ease-in-out infinite; }
@keyframes gcfte-blink { 50% { opacity: 0; } }
@keyframes gcfte-pulse { 50% { opacity: .35; } }`;

const DEFAULT_GROUPS: FooterTerminalGrupo[] = [
  { dir: "producto/", links: [{ label: "funciones.md" }, { label: "precios.md" }, { label: "cambios.log" }] },
  { dir: "empresa/", links: [{ label: "nosotros.md" }, { label: "contacto.md" }, { label: "trabajo.md" }] },
  { dir: "legal/", links: [{ label: "privacidad.txt" }, { label: "terminos.txt" }] },
];

export function FooterTerminal({ user = "visitante@sitio", groups = DEFAULT_GROUPS, status = "todos los sistemas operativos", legal = "© 2026 · hecho con cariño y café", className = "", style }: FooterTerminalProps) {
  return (
    <footer className={`gcfte-root ${className}`} style={style}>
      <style>{CSS}</style>
      <p className="gcfte-prompt">
        <b>{user}</b>:~$ tree --mapa-del-sitio
      </p>
      <div className="gcfte-tree">
        {groups.map((group) => (
          <nav key={group.dir} aria-label={group.dir}>
            <p className="gcfte-dir">{group.dir}</p>
            <ul className="gcfte-list">
              {group.links.map((link) => (
                <li key={link.label}>
                  <a href={link.href ?? "#"} onClick={(event) => !link.href && event.preventDefault()}>
                    {link.label}
                  </a>
                </li>
              ))}
            </ul>
          </nav>
        ))}
      </div>
      <p className="gcfte-prompt">
        <b>{user}</b>:~$ <span className="gcfte-caret" aria-hidden="true" />
      </p>
      <div className="gcfte-foot">
        <span>
          <i className="gcfte-dot" aria-hidden="true" />
          {status}
        </span>
        <span>{legal}</span>
      </div>
    </footer>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

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

Ejemplo de uso de Footer de terminal con árbol del sitio

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

ejemplo.tsx5 líneas
import { FooterTerminal } from "./footer-terminal";

export default function Ejemplo() {
  return <FooterTerminal user="Ejemplo" />;
}

Propiedades de FooterTerminal

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

PropiedadTipoDescripción
user?stringOpcional.
groups?FooterTerminalGrupo[]Opcional.
status?stringOpcional.
legal?stringOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 81 líneas (4 KB) y define 2 animaciones con @keyframes. Esto es lo que hace Footer de terminal con árbol del sitio 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)