Mundo skeuomórfico: mesa de mezclas
Paneles de madera, potenciómetros, faders, vúmetros de LED animados y botones de silencio con luz; operable con teclado.
Mezcladora 3 canales
Voz
Guitarra
Batería
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useEffect, useState, type CSSProperties } from "react";
/** Mundo skeuomórfico: mesa de mezclas con paneles de madera, canales con potenciómetro, fader, vúmetro de LED animado y botón de silencio con luz. Todo es operable con teclado. React + TypeScript, sin dependencias. */
export type MundoSkeuoAudioProps = {
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcmsa-root { box-sizing: border-box; container-type: inline-size; width: 100%; padding: clamp(14px, 3cqi, 28px); background: radial-gradient(120% 120% at 50% 0%, #3a2c22, #1b140f); font: 700 10px/1 "Helvetica Neue", Arial, ui-sans-serif, sans-serif; letter-spacing: .16em; text-transform: uppercase; color: #c9ccd4; }
.gcmsa-root *, .gcmsa-root *::before, .gcmsa-root *::after { box-sizing: border-box; }
.gcmsa-root button { font: inherit; letter-spacing: inherit; text-transform: inherit; }
.gcmsa-root :focus-visible { outline: 2px solid #5aa9ff; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .gcmsa-root * { transition-duration: .01ms !important; } }
.gcmsa-desk { display: grid; grid-template-columns: 22px 1fr 22px; max-width: 700px; margin: 0 auto; border-radius: 14px; overflow: hidden; box-shadow: 0 30px 50px -20px rgba(0, 0, 0, .8), 0 0 0 1px #0b0805; }
.gcmsa-wood { background: repeating-linear-gradient(90deg, #7a4a2a 0 3px, #6d4125 3px 6px, #82522f 6px 9px), #6d4125; box-shadow: inset 0 0 12px rgba(0, 0, 0, .6); }
.gcmsa-body { padding: 18px 16px; background: repeating-linear-gradient(90deg, rgba(255, 255, 255, .04) 0 1px, transparent 1px 3px), linear-gradient(180deg, #4a4d55, #2f3238); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .2); }
.gcmsa-title { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; color: #e8eaf0; }
.gcmsa-title i { display: block; width: 10px; height: 10px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #d8ffd2, #32d34a 60%, #14912a); box-shadow: 0 0 10px 2px rgba(50, 211, 74, .6); }
.gcmsa-strips { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.gcmsa-strip { display: grid; justify-items: center; gap: 14px; padding: 14px 8px 16px; border-radius: 8px; background: linear-gradient(180deg, #3b3e46, #2a2c33); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12), inset 0 -2px 4px rgba(0, 0, 0, .5); }
.gcmsa-knob { position: relative; width: 62px; height: 62px; border-radius: 50%; background: conic-gradient(from 0deg, #f1f2f5, #a9acb6, #f1f2f5, #9a9da8, #f1f2f5); box-shadow: 0 6px 8px rgba(0, 0, 0, .6), inset 0 0 0 2px rgba(0, 0, 0, .4); cursor: ns-resize; touch-action: none; outline: 0; }
.gcmsa-knob:focus-visible { box-shadow: 0 6px 8px rgba(0, 0, 0, .6), inset 0 0 0 2px rgba(0, 0, 0, .4), 0 0 0 3px #5aa9ff; }
.gcmsa-knob::before { content: ""; position: absolute; inset: 7px; border-radius: 50%; background: linear-gradient(145deg, #e6e8ee, #b6b9c3); box-shadow: inset 0 2px 3px rgba(255, 255, 255, .8), inset 0 -2px 4px rgba(0, 0, 0, .25); }
.gcmsa-knob i { position: absolute; left: 50%; top: 7px; width: 4px; height: 16px; margin-left: -2px; border-radius: 2px; background: #1b1c21; }
.gcmsa-meter { display: grid; grid-template-rows: repeat(12, 1fr); gap: 3px; width: 14px; height: 130px; padding: 4px; border-radius: 4px; background: #0b0c0e; box-shadow: inset 0 2px 4px #000, 0 1px 0 rgba(255, 255, 255, .12); }
.gcmsa-meter i { border-radius: 1px; background: color-mix(in srgb, var(--c) 18%, #131418); }
.gcmsa-meter i[data-on="true"] { background: var(--c); box-shadow: 0 0 6px var(--c); }
.gcmsa-fader { -webkit-appearance: slider-vertical; appearance: slider-vertical; writing-mode: vertical-lr; direction: rtl; width: 30px; height: 120px; margin: 0; background: transparent; accent-color: #d6d8e0; }
.gcmsa-mute { display: grid; place-items: center; width: 100%; min-height: 36px; border: 0; border-radius: 6px; background: linear-gradient(180deg, #5a5d66, #3c3f47); color: #e8eaf0; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 2px 0 #14151a; }
.gcmsa-mute:active { transform: translateY(1px); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25); }
.gcmsa-mute[aria-pressed="true"] { color: #ff8a7a; text-shadow: 0 0 8px rgba(255, 90, 70, .8); }
.gcmsa-name { color: #aeb1bb; }
@container (max-width: 520px) { .gcmsa-strips { gap: 6px; } .gcmsa-knob { width: 50px; height: 50px; } }`;
type Ch = { name: string; gain: number; level: number; mute: boolean };
export function MundoSkeuoAudio({ className = "", style }: MundoSkeuoAudioProps) {
const [ch, setCh] = useState<Ch[]>([{ name: "Voz", gain: 60, level: 70, mute: false }, { name: "Guitarra", gain: 45, level: 55, mute: false }, { name: "Batería", gain: 72, level: 80, mute: false }]);
const [meters, setMeters] = useState([0.5, 0.4, 0.6]);
useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const id = window.setInterval(() => !document.hidden && setMeters((m) => m.map((v) => Math.max(0.05, Math.min(1, v + (Math.random() - 0.5) * 0.5)))), 120);
return () => window.clearInterval(id);
}, []);
const set = (i: number, patch: Partial<Ch>) => setCh((list) => list.map((c, n) => (n === i ? { ...c, ...patch } : c)));
const color = (n: number) => (n > 9 ? "#ff4d3a" : n > 7 ? "#ffb82e" : "#38e07b");
return (
<div className={`gcmsa-root ${className}`} style={style}>
<style>{CSS}</style>
<div className="gcmsa-desk">
<div className="gcmsa-wood" aria-hidden="true" />
<div className="gcmsa-body">
<div className="gcmsa-title">
<span>Mezcladora 3 canales</span>
<i aria-hidden="true" />
</div>
<div className="gcmsa-strips">
{ch.map((c, i) => {
const lit = c.mute ? 0 : Math.round(meters[i] * (c.level / 100) * 12 * (0.5 + c.gain / 120));
return (
<div key={c.name} className="gcmsa-strip">
<div
className="gcmsa-knob"
role="slider"
tabIndex={0}
aria-label={`Ganancia ${c.name}`}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={c.gain}
style={{ transform: `rotate(${(c.gain / 100) * 270 - 135}deg)` }}
onKeyDown={(e) => {
if (e.key === "ArrowUp" || e.key === "ArrowRight") { e.preventDefault(); set(i, { gain: Math.min(100, c.gain + 4) }); }
if (e.key === "ArrowDown" || e.key === "ArrowLeft") { e.preventDefault(); set(i, { gain: Math.max(0, c.gain - 4) }); }
}}
onPointerDown={(e) => { (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); (e.currentTarget as HTMLElement).dataset.y = String(e.clientY); (e.currentTarget as HTMLElement).dataset.g = String(c.gain); }}
onPointerMove={(e) => { const el = e.currentTarget as HTMLElement; if (e.buttons === 1 && el.dataset.y) set(i, { gain: Math.max(0, Math.min(100, Number(el.dataset.g) + (Number(el.dataset.y) - e.clientY) * 0.8)) }); }}
>
<i />
</div>
<div className="gcmsa-meter" aria-hidden="true">
{Array.from({ length: 12 }, (_, n) => {
const k = 11 - n;
return <i key={n} data-on={k < lit} style={{ "--c": color(k) } as CSSProperties} />;
})}
</div>
<input className="gcmsa-fader" type="range" min={0} max={100} value={c.level} aria-label={`Volumen ${c.name}`} onChange={(e) => set(i, { level: Number(e.target.value) })} />
<button className="gcmsa-mute" type="button" aria-pressed={c.mute} onClick={() => set(i, { mute: !c.mute })}>
{c.mute ? "Silenciado" : "Mute"}
</button>
<span className="gcmsa-name">{c.name}</span>
</div>
);
})}
</div>
</div>
<div className="gcmsa-wood" aria-hidden="true" />
</div>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/mundo-skeuo-audioEjemplo de uso de Mundo skeuomórfico: mesa de mezclas
Después de copiar mundo-skeuo-audio.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { MundoSkeuoAudio } from "./mundo-skeuo-audio";
export default function Ejemplo() {
return <MundoSkeuoAudio />;
}Propiedades de MundoSkeuoAudio
MundoSkeuoAudio acepta 2 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 107 líneas (8 KB) y no necesita @keyframes. Esto es lo que hace Mundo skeuomórfico: mesa de mezclas 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: slider.
- Responde al teclado, no solo al ratón.
- Reacciona al puntero con eventos pointer (ratón, lápiz y táctil).
- 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".