Ir al contenido
Evaluar mi proyecto
Componentes · Mundos de estiloDisponible

Mundo neobrutalista: panel de proyecto

Pestañas, lista de tareas con etiquetas, barra de avance y botones con sombra dura que se hunden; todo funciona.

Proyecto: Tienda nueva33%
Avance del proyecto

Código completo

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

mundo-neobrutal.tsx91 líneas
Descargar
"use client";

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

/** Mundo neobrutalista: panel de proyecto con pestañas, lista de tareas, etiquetas, barra de avance y botones con sombra dura que se hunden. Los controles funcionan. React + TypeScript, sin dependencias. */
export type MundoNeobrutalProps = {
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcmnb-root { box-sizing: border-box; container-type: inline-size; width: 100%; padding: clamp(14px, 4cqi, 36px); background: #ffe14d; color: #111; font: 800 15px/1.3 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcmnb-root *, .gcmnb-root *::before, .gcmnb-root *::after { box-sizing: border-box; }
.gcmnb-root button, .gcmnb-root input { font: inherit; }
.gcmnb-root :focus-visible { outline: 4px solid #2f5bff; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .gcmnb-root * { transition-duration: .01ms !important; } }
.gcmnb-win { max-width: 560px; margin: 0 auto; border: 4px solid #111; background: #fff; box-shadow: 8px 8px 0 #111; }
.gcmnb-bar { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 4px solid #111; background: #ff90e8; text-transform: uppercase; letter-spacing: .06em; font-size: 13px; }
.gcmnb-tabs { display: flex; border-bottom: 4px solid #111; }
.gcmnb-tab { flex: 1; min-height: 46px; border: 0; border-right: 4px solid #111; background: #fff; font-weight: 900; text-transform: uppercase; letter-spacing: .04em; cursor: pointer; }
.gcmnb-tab:last-child { border-right: 0; }
.gcmnb-tab:hover { background: #fff3b0; }
.gcmnb-tab[aria-selected="true"] { background: #7cf5c3; }
.gcmnb-body { display: grid; gap: 16px; padding: 18px; }
.gcmnb-prog { display: grid; gap: 6px; }
.gcmnb-track { height: 26px; border: 4px solid #111; background: #fff; }
.gcmnb-track i { display: block; height: 100%; background: repeating-linear-gradient(45deg, #2f5bff 0 10px, #5b83ff 10px 20px); border-right: 4px solid #111; transition: width .4s cubic-bezier(.34, 1.4, .64, 1); }
.gcmnb-list { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }
.gcmnb-task { display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 12px; border: 3px solid #111; background: #fff; text-align: left; cursor: pointer; box-shadow: 4px 4px 0 #111; transition: transform .1s, box-shadow .1s, background .1s; }
.gcmnb-task:hover { background: #fff3b0; }
.gcmnb-task:active { transform: translate(4px, 4px); box-shadow: 0 0 0 #111; }
.gcmnb-chk { flex: none; display: grid; place-items: center; width: 24px; height: 24px; border: 3px solid #111; background: #fff; font-size: 16px; line-height: 1; }
.gcmnb-task[aria-pressed="true"] .gcmnb-chk { background: #7cf5c3; }
.gcmnb-task[aria-pressed="true"] .gcmnb-t { text-decoration: line-through; text-decoration-thickness: 3px; opacity: .6; }
.gcmnb-tag { margin-left: auto; padding: 2px 8px; border: 3px solid #111; background: var(--c); font-size: 11px; text-transform: uppercase; }
.gcmnb-add { display: flex; gap: 10px; }
.gcmnb-in { flex: 1; min-width: 0; min-height: 46px; padding: 0 12px; border: 4px solid #111; background: #fff; box-shadow: 4px 4px 0 #111; }
.gcmnb-in::placeholder { color: #777; font-weight: 700; }
.gcmnb-go { min-height: 46px; padding: 0 18px; border: 4px solid #111; background: #ffe14d; font-weight: 900; text-transform: uppercase; cursor: pointer; box-shadow: 4px 4px 0 #111; transition: transform .1s, box-shadow .1s; }
.gcmnb-go:active { transform: translate(4px, 4px); box-shadow: 0 0 0 #111; }
.gcmnb-empty { padding: 28px 10px; text-align: center; border: 3px dashed #111; }`;

const TAGS = ["#ff90e8", "#8bc8ff", "#7cf5c3", "#ff9a8a"];

export function MundoNeobrutal({ className = "", style }: MundoNeobrutalProps) {
  const [tab, setTab] = useState<"todas" | "pendientes" | "hechas">("todas");
  const [tasks, setTasks] = useState([{ t: "Diseñar el logo", d: true, g: "Diseño" }, { t: "Escribir los textos", d: false, g: "Contenido" }, { t: "Montar la tienda", d: false, g: "Código" }]);
  const [text, setText] = useState("");
  const done = tasks.filter((t) => t.d).length;
  const pct = tasks.length ? Math.round((done / tasks.length) * 100) : 0;
  const shown = tasks.map((t, i) => ({ ...t, i })).filter((t) => (tab === "pendientes" ? !t.d : tab === "hechas" ? t.d : true));
  return (
    <div className={`gcmnb-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div className="gcmnb-win">
        <div className="gcmnb-bar">
          <span>Proyecto: Tienda nueva</span>
          <span>{pct}%</span>
        </div>
        <div className="gcmnb-tabs" role="tablist" aria-label="Filtro">
          {([["todas", "Todas"], ["pendientes", "Pendientes"], ["hechas", "Hechas"]] as const).map(([id, label]) => (
            <button key={id} className="gcmnb-tab" type="button" role="tab" aria-selected={tab === id} onClick={() => setTab(id)}>{label}</button>
          ))}
        </div>
        <div className="gcmnb-body">
          <div className="gcmnb-prog">
            <span>Avance del proyecto</span>
            <div className="gcmnb-track" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={pct} aria-label="Avance"><i style={{ width: `${pct}%` }} /></div>
          </div>
          <ul className="gcmnb-list">
            {shown.map((task) => (
              <li key={task.t + task.i}>
                <button className="gcmnb-task" type="button" aria-pressed={task.d} onClick={() => setTasks((list) => list.map((x, n) => (n === task.i ? { ...x, d: !x.d } : x)))}>
                  <span className="gcmnb-chk" aria-hidden="true">{task.d ? "✓" : ""}</span>
                  <span className="gcmnb-t">{task.t}</span>
                  <span className="gcmnb-tag" style={{ "--c": TAGS[task.i % TAGS.length] } as CSSProperties}>{task.g}</span>
                </button>
              </li>
            ))}
            {shown.length === 0 ? <li className="gcmnb-empty">Nada por aquí. ¡Respira!</li> : null}
          </ul>
          <form className="gcmnb-add" onSubmit={(e) => { e.preventDefault(); const t = text.trim(); if (!t) return; setTasks((l) => [...l, { t, d: false, g: "Nueva" }]); setText(""); }}>
            <input className="gcmnb-in" aria-label="Nueva tarea" placeholder="Nueva tarea…" value={text} onChange={(e) => setText(e.target.value)} />
            <button className="gcmnb-go" type="submit">Añadir</button>
          </form>
        </div>
      </div>
    </div>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

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

Ejemplo de uso de Mundo neobrutalista: panel de proyecto

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

ejemplo.tsx5 líneas
import { MundoNeobrutal } from "./mundo-neobrutal";

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

Propiedades de MundoNeobrutal

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

PropiedadTipoDescripción
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 91 líneas (6 KB) y no necesita @keyframes. Esto es lo que hace Mundo neobrutalista: panel de proyecto 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).
  • Declara roles ARIA para lectores de pantalla: tablist, tab, progressbar.
  • 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)