Ir al contenido
Evaluar mi proyecto
Componentes · Mundos de estiloDisponible

Mundo claymorfismo: app de tareas

Avatar, anillo de avance, tarjetas de plastilina con casillas que rebotan y botón flotante; las tareas se completan de verdad.

Buenos díasAna

2 tareas por terminar hoy

Código completo

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

mundo-arcilla.tsx87 líneas
Descargar
"use client";

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

/** Mundo claymorfismo: una app de tareas con avatar, anillo de avance, tarjetas de plastilina con casillas que rebotan y un botón flotante. Las tareas se completan de verdad. React + TypeScript, sin dependencias. */
export type MundoArcillaProps = {
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcmar-root { box-sizing: border-box; container-type: inline-size; width: 100%; padding: clamp(18px, 4cqi, 40px); background: #fdeee3; color: #40284d; font: 700 15px/1.3 ui-rounded, "SF Pro Rounded", "Nunito", ui-sans-serif, system-ui, sans-serif; }
.gcmar-root *, .gcmar-root *::before, .gcmar-root *::after { box-sizing: border-box; }
.gcmar-root button { font: inherit; -webkit-tap-highlight-color: transparent; }
.gcmar-root :focus-visible { outline: 3px solid #40284d; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .gcmar-root * { transition-duration: .01ms !important; animation: none !important; } }
.gcmar-app { position: relative; max-width: 420px; margin: 0 auto; padding: 24px 20px 90px; border-radius: 44px; background: #ffe4d2; box-shadow: inset -10px -14px 24px rgba(214, 150, 100, .35), inset 10px 14px 24px rgba(255, 255, 255, .75), 20px 26px 40px -14px rgba(190, 110, 60, .45); }
.gcmar-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.gcmar-hi { margin: 0; font: 900 24px/1.1 ui-rounded, "SF Pro Rounded", "Nunito", ui-sans-serif, sans-serif; letter-spacing: -.02em; }
.gcmar-hi span { display: block; font-size: 14px; font-weight: 700; color: #8a5a6a; }
.gcmar-av { display: grid; place-items: center; width: 52px; height: 52px; border-radius: 50%; background: #c3b1ff; color: #2e2160; font-weight: 900; font-size: 20px; box-shadow: inset -4px -5px 8px rgba(80, 50, 160, .35), inset 4px 5px 8px rgba(255, 255, 255, .6), 3px 6px 10px -3px rgba(80, 50, 160, .5); }
.gcmar-sum { display: flex; align-items: center; gap: 18px; padding: 18px; margin-bottom: 18px; border-radius: 30px; background: #ffb08a; box-shadow: inset -8px -10px 18px rgba(190, 80, 40, .35), inset 8px 10px 18px rgba(255, 255, 255, .5), 10px 14px 22px -8px rgba(190, 80, 40, .45); }
.gcmar-ring { position: relative; width: 84px; height: 84px; flex: none; }
.gcmar-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.gcmar-ring circle { fill: none; stroke-width: 12; stroke-linecap: round; }
.gcmar-ring .t { stroke: rgba(255, 255, 255, .45); }
.gcmar-ring .v { stroke: #fff7ee; transition: stroke-dashoffset .6s cubic-bezier(.34, 1.3, .64, 1); filter: drop-shadow(2px 3px 2px rgba(190, 80, 40, .4)); }
.gcmar-ring b { position: absolute; inset: 0; display: grid; place-items: center; font-weight: 900; font-size: 20px; font-variant-numeric: tabular-nums; }
.gcmar-sum p { margin: 0; font-size: 17px; line-height: 1.25; }
.gcmar-list { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }
.gcmar-task { display: flex; align-items: center; gap: 14px; width: 100%; padding: 14px 16px; border: 0; border-radius: 24px; background: var(--c); color: inherit; text-align: left; cursor: pointer; box-shadow: inset -6px -8px 14px color-mix(in srgb, var(--c) 62%, #4a1030), inset 6px 8px 14px color-mix(in srgb, var(--c) 35%, #fff), 8px 12px 18px -8px color-mix(in srgb, var(--c) 70%, #3a0a20); transition: transform .35s cubic-bezier(.34, 1.56, .64, 1); }
.gcmar-task:hover { transform: translateY(-3px); }
.gcmar-task:active { transform: scale(.97, .95); }
.gcmar-chk { flex: none; display: grid; place-items: center; width: 30px; height: 30px; border-radius: 50%; background: #fff7ee; box-shadow: inset -3px -4px 6px rgba(150, 90, 60, .35), inset 3px 4px 6px #fff, 2px 3px 5px rgba(60, 30, 80, .25); color: transparent; transition: background .3s, color .2s, transform .4s cubic-bezier(.34, 1.8, .64, 1); }
.gcmar-task[aria-pressed="true"] .gcmar-chk { background: #3dd6a3; color: #0d4a37; transform: scale(1.12); }
.gcmar-task[aria-pressed="true"] .gcmar-t { text-decoration: line-through; opacity: .6; }
.gcmar-fab { position: absolute; right: 22px; bottom: 22px; display: grid; place-items: center; width: 62px; height: 62px; border: 0; border-radius: 50%; background: #40284d; color: #fff; font-size: 30px; cursor: pointer; box-shadow: inset -5px -7px 12px rgba(0, 0, 0, .45), inset 5px 7px 12px rgba(255, 255, 255, .25), 6px 10px 16px -4px rgba(64, 40, 77, .6); transition: transform .35s cubic-bezier(.34, 1.56, .64, 1); }
.gcmar-fab:hover { transform: rotate(90deg) scale(1.06); }
.gcmar-fab:active { transform: scale(.9); }`;

const COLORS = ["#c9f0e0", "#d8ccff", "#ffe8a8", "#ffd0dc", "#cfe6ff"];
const NAMES = ["Revisar la propuesta", "Llamar al cliente", "Preparar la demostración", "Enviar la factura", "Agendar la reunión"];

export function MundoArcilla({ className = "", style }: MundoArcillaProps) {
  const [tasks, setTasks] = useState([{ t: "Revisar la propuesta", d: true }, { t: "Llamar al cliente", d: false }, { t: "Preparar la demostración", d: false }]);
  const done = tasks.filter((t) => t.d).length;
  const C = 2 * Math.PI * 36;
  const pct = tasks.length ? done / tasks.length : 0;
  return (
    <div className={`gcmar-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div className="gcmar-app">
        <div className="gcmar-top">
          <h3 className="gcmar-hi">
            <span>Buenos días</span>
            Ana
          </h3>
          <span className="gcmar-av" aria-hidden="true">A</span>
        </div>
        <div className="gcmar-sum">
          <div className="gcmar-ring" role="img" aria-label={`${done} de ${tasks.length} tareas hechas`}>
            <svg viewBox="0 0 84 84" aria-hidden="true">
              <circle className="t" cx="42" cy="42" r="36" />
              <circle className="v" cx="42" cy="42" r="36" strokeDasharray={C} strokeDashoffset={C * (1 - pct)} />
            </svg>
            <b>{Math.round(pct * 100)}%</b>
          </div>
          <p>{done === tasks.length && tasks.length ? "¡Todo listo por hoy!" : `${tasks.length - done} tareas por terminar hoy`}</p>
        </div>
        <ul className="gcmar-list">
          {tasks.map((task, i) => (
            <li key={task.t + i}>
              <button className="gcmar-task" type="button" aria-pressed={task.d} style={{ "--c": COLORS[i % COLORS.length] } as CSSProperties} onClick={() => setTasks((list) => list.map((x, n) => (n === i ? { ...x, d: !x.d } : x)))}>
                <span className="gcmar-chk" aria-hidden="true">
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5" /></svg>
                </span>
                <span className="gcmar-t">{task.t}</span>
              </button>
            </li>
          ))}
        </ul>
        <button className="gcmar-fab" type="button" aria-label="Añadir tarea" onClick={() => setTasks((list) => [...list, { t: NAMES[(list.length + 1) % NAMES.length], d: false }])}>+</button>
      </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-arcilla

Ejemplo de uso de Mundo claymorfismo: app de tareas

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

ejemplo.tsx5 líneas
import { MundoArcilla } from "./mundo-arcilla";

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

Propiedades de MundoArcilla

MundoArcilla 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 87 líneas (7 KB) y no necesita @keyframes. Esto es lo que hace Mundo claymorfismo: app de tareas 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: img.
  • Calcula tonos con color-mix(), así que un solo color base alcanza.
  • 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)