Cómo construimos el editor e inspector interactivo de PonlOnline con Vue y Pinia

Un desglose técnico de cómo diseñamos una demostración de código en vivo sin recurrir a funciones inseguras como eval(), integrando accesibilidad por teclado conforme a WAI-ARIA y sincronizando el estado global mediante un almacén reactivo de Pinia.

LO QUE APRENDERÁS EN ESTA NOTA TÉCNICA
  • Desacoplamiento con Pinia: Por qué un almacén global resuelve la interacción entre cabecera, banners y editor sin prop drilling.
  • Previsualización segura: Sincronización de tokens CSS y reactividad mediante directivas nativas de Vue en lugar de inyecciones dinámicas de scripts.
  • Patrón WAI-ARIA Tablist: Gestión accesible de foco y navegación con teclado (flechas, Inicio y Fin).
  • Inspector en cascada: Uso de atributos de datos y pseudo-elementos CSS para inspeccionar la estructura real de la página.

1. El reto: una demostración pedagógica sin riesgos de seguridad

En la portada de PonlOnline queríamos mostrar cómo una idea abstracta se transforma en un sitio web mediante HTML, CSS y JavaScript/Vue. Muchas herramientas interactivas de código en la web recurren a eval(), iframes dinámicos o inyecciones de cadenas en etiquetas <style> para reflejar los cambios en pantalla.

Aunque esas técnicas son válidas para entornos de prueba aislados (playgrounds completos), en una página comercial de producción introducen tres problemas críticos:

  1. Riesgos de seguridad y XSS: Ejecutar código arbitrario del usuario en el contexto principal del navegador expone la sesión y contamina el DOM.
  2. Sobrecarga de rendimiento: Crear y destruir iframes o recompilar hojas de estilo en cada pulsación de tecla genera retrasos perceptibles (layout thrashing).
  3. Falta de integración con la accesibilidad: La previsualización debe ser legible para tecnologías de asistencia y manejable íntegramente con teclado.

Decidimos diseñar un sistema cerrado y puramente reactivo compuesto por dos piezas: el almacén de estado en src/stores/experience.ts y el componente interactivo en src/components/CodeLab.vue.

2. Estado compartido con Pinia (src/stores/experience.ts)

El modo inspector no vive únicamente dentro de la caja de código. En PonlOnline, el botón «Inspeccionar» de la cabecera fija, el banner educativo tras la sección de proceso y el propio previsualizador necesitan leer y alternar el mismo estado.

Usar propiedades locales (ref dentro de HomePage.vue) habría obligado a propagar eventos y propiedades a través de múltiples niveles de componentes intermedios. Definimos un almacén centralizado usando la sintaxis de Setup Store de Pinia:

src/stores/experience.tsTypeScript
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useExperienceStore = defineStore('experience', () => {
  const inspector = ref(false)
  const activeTab = ref<'html' | 'css' | 'vue'>('html')
  const accent = ref<'green' | 'violet'>('green')
  const rounded = ref(true)
  const idea = ref('Tu próxima gran idea')

  function toggleInspector() { inspector.value = !inspector.value }
  function resetDemo() {
    accent.value = 'green'
    rounded.value = true
    idea.value = 'Tu próxima gran idea'
  }

  return { inspector, activeTab, accent, rounded, idea, toggleInspector, resetDemo }
})

Ventaja de diseño:

Al aislar accent, rounded e idea en Pinia, la función resetDemo() permite restablecer el estado inicial en una única operación atómica sin que ningún componente quede desincronizado.

3. Sincronización reactiva sin inyección de código

En la pestaña style.css del editor, el usuario puede alternar la paleta de color entre verde y violeta, así como activar o desactivar los bordes redondeados.

En vez de parsear texto CSS dinámico, vinculamos las propiedades reactivas del almacén directamente con clases semánticas y propiedades personalizadas en la plantilla del componente:

src/components/CodeLab.vue (fragmento de previsualización)Vue Template
<!-- Sincronización de clases dinámicas según el estado reactivo -->
<div
  class="mini-site tu-negocio"
  :class="{ violet: experience.accent === 'violet', square: !experience.rounded }"
>
  <h3>{{ experience.idea.trim() || 'Tu próxima gran idea' }}<span>.</span></h3>
  <p>{{ launched ? 'El primer paso es imaginarlo...' : 'De la idea a lo increíble.' }}</p>
</div>

En el código que se muestra en el panel superior, usamos interpolación de texto de solo lectura para ilustrar los valores CSS calculados:

src/components/CodeLab.vue (fragmento de editor simulado)CSS Syntax Highlighting
<div>
  <span>&nbsp; background: {{ experience.accent === 'green' ? '#20271a' : '#28232f' }};</span>
</div>
<div>
  <span>&nbsp; border-radius: <span class="syntax-purple">{{ experience.rounded ? '12px' : '0px' }}</span>;</span>
</div>

El usuario percibe que está editando CSS en tiempo real, mientras el motor de renderizado de Vue aplica parches hiperoptimizados sobre el árbol de nodos DOM sin recalcular estilos ajenos ni recompilar hojas de estilo externas.

4. Accesibilidad y patrón WAI-ARIA Tablist

Uno de los fallos más habituales en editores interactivos es atrapar el foco del teclado o impedir la navegación mediante teclas de cursor estándar.

Implementamos la recomendación de la especificación WAI-ARIA para listas de pestañas (Tablist):

  • El contenedor padre tiene role="tablist" y una etiqueta descriptiva (aria-label="Explorar tecnologías").
  • Cada botón tiene role="tab", :aria-selected="experience.activeTab === tab.id" y :aria-controls="`panel-${tab.id}`".
  • Aplicamos el patrón roving tabindex: únicamente la pestaña activa tiene tabindex="0"; las pestañas inactivas tienen tabindex="-1".
src/components/CodeLab.vue (gestión de teclado)TypeScript
async function moveTab(event: KeyboardEvent, index: number) {
  if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return
  event.preventDefault()

  const next = event.key === 'Home'
    ? 0
    : event.key === 'End'
      ? tabs.length - 1
      : (index + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length

  experience.activeTab = tabs[next]!.id
  await nextTick()
  document.getElementById(`tab-${experience.activeTab}`)?.focus()
}

Al pulsar las flechas izquierda o derecha, el foco se transfiere de forma cíclica al siguiente control y nextTick() asegura que el elemento del DOM correspondiente haya recibido el atributo tabindex="0" antes de invocar .focus().

5. El modo inspector: descubriendo HTML real con pseudo-elementos

Cuando el usuario activa el modo inspector (desde la cabecera o desde el botón flotante), la clase .inspector-on se añade al contenedor raíz .site-shell.

Aprovechamos las propiedades de los pseudo-elementos CSS y la función attr() para proyectar etiquetas sin alterar el contenido textual accesible de las secciones:

src/style.css (inspector en CSS)CSS
.inspector-on .section-inspect {
  outline: 1px dashed #a79bea88;
  outline-offset: -2px;
}

.inspector-on .section-inspect::after {
  content: attr(data-element);
  position: absolute;
  top: 0;
  left: 16px;
  font: 12px var(--font-mono);
  color: #d9cff9;
  background: #393047;
  padding: 5px 10px;
  pointer-events: none;
  z-index: 3;
}

De esta manera, secciones como <section id="servicios" data-element="<section id='servicios'>"> muestran su propia definición técnica flotante de forma no invasiva, cumpliendo una función pedagógica inmediata.

6. Decisiones de diseño y límites intencionales

El desarrollo de software profesional consiste en saber qué incluir y qué omitir. Documentamos aquí los límites conscientes de esta implementación:

  • Límite de caracteres en la entrada: El campo #idea-input está acotado con maxlength="36". En pantallas móviles de 320px a 375px, una cadena arbitrariamente larga desbordaría el contenedor visual y provocaría scroll horizontal no deseado.
  • Transformación CSS condicional: En escritorio, el editor tiene una sutil rotación (transform: rotate(1deg)) que aporta dinamismo. En dispositivos móviles de menos de 540px, la media query anula la rotación (transform: none) para evitar pérdida de nitidez en pantallas de baja densidad de píxeles.
  • Sin persistencia local permanente: El estado de la demostración se reinicia al recargar la página. Para una landing comercial, esto garantiza que cada visitante comience siempre con la experiencia visual limpia y equilibrada.