Ir al contenido
solo en desarrollo, cero dependencias

Edita tu app en el navegador.
Que tu agente escriba el código.

Apunta a cualquier elemento, ajústalo y entrégale a tu agente un diff en JSON que sí puede aplicar.

pnpm add -D @guerrerosoconm/visual-editor
dependencias
0
licencia
MIT
versión
v0.4.0
hosts
React, Vue, Angular, vanilla

Cuatro pasos, y ninguno consiste en describir el cambio en un prompt.

Señalar es más rápido que escribir un párrafo sobre lo que estabas señalando. Todo el trabajo del editor consiste en convertir ese gesto en algo sobre lo que un agente pueda actuar sin adivinar.

  1. 1

    Hover

    Cada elemento se resalta con su etiqueta, su componente y su tamaño. Recorre el árbol con las flechas cuando lo que buscas está tres divs más arriba.

  2. 2

    Ajusta

    Haz clic y se abre el panel. Espaciado, relleno, trazo, tipografía, sombras, estados de hover. Los cambios se previsualizan como estilos inline, así que todavía no hay nada escrito.

  3. 3

    Guarda

    Cada elemento que tocas se escribe en edits.json con el antes, el después y suficientes anclas para encontrar el JSX que lo produjo.

  4. 4

    Pásaselo

    Tu agente lee el archivo, encuentra el source y escribe el cambio en tu sistema de estilos. Tailwind, CSS modules, lo que uses de verdad.

Arranca con una sola llamada en cualquier cosa.

El editor inyecta sus propios estilos, pinta en su propia raíz y no lee nada del host. Entre frameworks solo cambia a cuánto de lo que encuentra le puede poner nombre.

El editor, por host
host nombres de componente en el payloadprops en vivo para editar el variantanclas de class, testid y selectorsource stamping
React yes yes a yes yes
Vue 3 yes yes a yes no b
Angular yes yes a yes no b
Svelte, Solid, HTML plano no no yes partial c

El payload está completo en todas las filas. Lo único que varía es la capa de nombres: sin nombres de componente, el agente recibe la class, el test id, el selector y, en JSX, el archivo y la línea. Eso sigue siendo la dirección entera.

El endpoint, por servidor
host edits.json en discomanifest de componentescómo recibes el lote
Next.js App Router yes yes se escribe al guardar
Remix yes yes se escribe al guardar
Hono yes yes se escribe al guardar
Astro yes yes se escribe al guardar
Node a secas yes yes se escribe al guardar
sin backend ninguno no no consola [AI-EDIT-REQUEST], o Copy JSON
a
Se leen de las tripas del build de desarrollo para rellenar los desplegables de Component. Llegan al payload como component.props, que necesita un manifest de cva o de tv, o uno que aportes tú.
b
El include por defecto del plugin es /\.[jt]sx$/ y lo que parsea es JSX, así que un single-file component .vue y una plantilla de Angular no se estampan. El resto de la fila no cambia.
c
El stamper es ciego al framework: reescribe cualquier etiqueta host en minúscula dentro de un archivo .jsx o .tsx, así que una app de Solid recibe stamps. Un archivo .svelte o una página .html escrita a mano no recibe ninguno.
vue, svelte, astro, html plano
import { mountVisualEditor } from '@guerrerosoconm/visual-editor/standalone';

if (import.meta.env.DEV) mountVisualEditor();

El adaptador de servidor devuelve handlers de Request a Response con estándares web, así que Next.js App Router, Remix, Hono, Astro y Node a secas funcionan con los mismos tres exports.


la parte que de verdad importa

Un payload más fiel gana a un panel más rico.

Capturar un montón de píxeles calculados lo hace cualquiera. Que el agente aplique tu cambio en lugar de adivinarlo depende de si el JSON llevaba la intención o solo las consecuencias.

píxeles: el agente tiene que adivinar
"styles": {
  "background-color": {
    "before": "rgb(15, 23, 42)",
    "after":  "rgb(239, 68, 68)"
  },
  "color": {
    "before": "rgb(248, 250, 252)",
    "after":  "rgb(255, 255, 255)"
  }
}
intención: el agente solo la aplica
"component": {
  "name": "Button",
  "file": "components/ui/button.tsx",
  "props": {
    "variant": {
      "before": "default",
      "after":  "destructive"
    }
  }
}

Esa segunda forma aparece cuando tus componentes usan cva. El servidor los escanea, el panel obtiene desplegables reales de variant y size, y cambiar uno intercambia las clases de verdad en el elemento. Un variant auténtico, no una imitación en CSS.


Deja de hacer que el agente busque tu JSX.

Añade el plugin de Vite y cada elemento lleva el archivo, la línea y la columna de su propia etiqueta. El agente deja de hacer grep y pasa a recibir la respuesta.

Solo para desarrollo por construcción: el plugin declara apply: 'serve', así que un build de producción no lo ejecuta nunca, diga lo que diga tu config. Sigue habiendo cero dependencias, porque parsea con el TypeScript que ya está en tu proyecto, cargado en diferido. Y sin él no se pierde nada: el payload se ancla en la ascendencia de componentes y en los nombres de clase igual que antes.

vite.config.ts
import { visualEditorSource } from '@guerrerosoconm/visual-editor/vite';

export default defineConfig({
  // Before react(): the JSX has to still be there to be stamped.
  plugins: [visualEditorSource(), react()],
});
ahora cada edit lleva esto
"source": "src/components/Button.tsx:42:7"

Un elemento guardado, todos los campos y lo que le aporta a tu agente.

Este es el artefacto que la herramienta existe para producir, completo y sin recortar para la página. Apunta a cualquier grupo de campos para ver las líneas que le pertenecen, o a una línea para ver para qué sirve.

edits.json, una entrada de 26 líneas
{ "timestamp": "2026-07-28T09:14:02.481Z", "url": "http://localhost:5173/dashboard", "viewport": { "width": 1440, "height": 900 }, "selector": "[data-testid=\"project-card-cta\"]", "tagName": "BUTTON", "className": "inline-flex items-center rounded-md px-3 py-2", "testId": "project-card-cta", "source": "src/components/ui/button.tsx:42:7", "sourceAmbiguous": true, "componentStack": ["Button", "ProjectCard", "DashboardPage"], "styles": { "padding-top": { "before": "8px", "after": "12px" }, "padding-bottom": { "before": "8px", "after": "12px" } }, "states": { "hover": { "background-color": "rgb(29, 78, 216)" } }, "text": { "before": "Open project", "after": "Open" }, "note": "Match the spacing scale we use on the hero CTA.", "component": { "name": "Button", "file": "src/components/ui/button.tsx", "props": { "variant": { "before": "default", "after": "destructive" } } }}

El panel existe para hacer decibles tres cosas.

Layout, espaciado, apariencia, relleno, trazo, tipografía y efectos, lo que esperarías de cualquier editor. Estas tres son las que cambian lo que el payload puede llevar.

El panel de estilos completo: breadcrumb, pestañas de estado, variants de componente, layout, espaciado, relleno, trazo, tipografía, efectos y el texto del elemento. Tres llamadas señalan las pestañas de estado, los desplegables de variant y size, y el botón Save edit.
  1. un panel por estado: hover, focus, active
  2. son variants de cva reales, no clases adivinadas
  3. Save edit escribe la entrada en edits.json

Pestañas de estado

Default, :hover, :focus y :active. Edita con una seleccionada y se exporta bajo states.hover en vez de styles. Un estilo de hover antes no se podía ni decir.

"states": { "hover": { "background-color": "rgb(29, 78, 216)" } }

Un undo que dice la verdad

Abarca toda la página y sigue el orden en que hiciste las cosas, así que deshacer algo de otro elemento lo selecciona y te lo enseña. Guardar no borra el historial, y el undo nunca entra en edits.json.

el elemento vuelve a quedar dirty, que es lo que ha pasado de verdad

Notas, con chincheta

Déjale al agente un mensaje sobre el porqué. Cada elemento que lleve una recibe una chincheta numerada en la página, porque una nota es la única edición que no cambia nada a la vista.

"note": "Match the spacing scale we use on the hero CTA."


arranque rápido

Tres pasos y ya estás editando.

1 . instala
pnpm add -D @guerrerosoconm/visual-editor
2 . móntalo detrás de un check de desarrollo
// app/layout.tsx
import { VisualEditor } from '@guerrerosoconm/visual-editor';

export default function RootLayout({ children }) {
  return (
    <html><body>
      {children}
      {process.env.NODE_ENV === 'development' && <VisualEditor />}
    </body></html>
  );
}
3 . expón el endpoint
// app/api/dev/visual-edits/route.ts
import { createVisualEditsHandlers } from '@guerrerosoconm/visual-editor/server';

export const { GET, POST, DELETE } = createVisualEditsHandlers();
!

El guard de NODE_ENV es lo que mantiene el editor fuera de tu bundle de producción, y el endpoint devuelve 404 fuera de desarrollo. No te lo saltes. Ya que estás, añade edits.json a tu .gitignore.

y luego pásale esto a tu agente
Read edits.json. For each edit, locate the element in the source using
source (when present), className, testId and componentStack, translate the
style values into our Tailwind classes, apply the changes, including any
states.hover / states.focus / states.active as the hover:/focus:/active:
variants, then clear the file.

Las preguntas que lo deciden.

¿Esto llega a producción?

Solo si lo montas sin condición, cosa que no deberías hacer. El endpoint, por su cuenta, devuelve 404 fuera de desarrollo.

¿Modifica mi código?

No. Escribe edits.json y nada más. Los cambios de código los hace tu agente, y ese diff lo revisas como cualquier otro.

¿Por qué recibo píxeles en vez de mis clases de Tailwind?

Porque el navegador solo conoce valores calculados. Por eso cada diff lleva before: para que el agente relacione before: 12px con el p-3 de tu JSX y elija la clase correcta.

No uso cva. ¿Sigue funcionando la detección de componentes?

La sección Component simplemente no aparece, y todo lo demás funciona. También puedes aportar tú el manifest. Mira antes la consola: cada definición que el scanner tuvo que descartar queda ahí registrada con el archivo y el motivo.

Mis ediciones han desaparecido.

Cerrar el editor descarta lo que no guardaste, pero antes pregunta, y el navegador también si recargas con algo pendiente. Una recarga completa sí lo borra todo, y es a propósito: después de que tu agente edite el código, la página debe mostrar ese código.

¿Cuánto cuesta?

Nada. MIT, cero dependencias en runtime, sin cuenta y sin telemetría. React y react-dom van como peers, nada más.

  • 0 dependencias en runtime react y react-dom como peers
  • MIT licencia tuya para hacer fork
  • ninguna telemetría sin cuenta, sin host externo

Hecho y mantenido por Marcos Guerreros Ocón. Una persona, a la vista de todos.

Cómo está construido Lo que aún no está hecho

Señálalo. Publícalo.

npm GitHub