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
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
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
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
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.
| host | nombres de componente en el payload | props en vivo para editar el variant | anclas de class, testid y selector | source 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.
| host | edits.json en disco | manifest de componentes | có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.
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.
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.
"styles": {
"background-color": {
"before": "rgb(15, 23, 42)",
"after": "rgb(239, 68, 68)"
},
"color": {
"before": "rgb(248, 250, 252)",
"after": "rgb(255, 255, 255)"
}
}
"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.
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()],
});
"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.
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.
- un panel por estado: hover, focus, active
- son variants de cva reales, no clases adivinadas
- 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."
Tres pasos y ya estás editando.
pnpm add -D @guerrerosoconm/visual-editor
// 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>
);
}
// 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.
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.