# Guía para programar en Publica con IA

Publica con IA publica en internet lo que programas para la persona: dashboards, páginas y apps web, con sus propios datos. Cada proyecto se ve en https://publica.sumaconia.com/<slug>. Sigue esta guía para que el código funcione a la primera.

## 1. Cómo es un sitio de Publica con IA

- Es un sitio estático: `index.html` en la raíz (obligatorio) y, si hace falta, más archivos, por ejemplo `estilos.css`, `js/app.js` o `img/logo.png`.
- Usa **rutas relativas** (`estilos.css`, `./js/app.js`, `img/logo.png`). Nunca rutas que empiecen con `/`.
- Puedes usar librerías de un CDN: cdnjs.cloudflare.com, cdn.jsdelivr.net o unpkg.com (Chart.js, D3, Leaflet, Tailwind, etc.). Pon la versión exacta en la URL.
- No hace falta compilar. Si el proyecto usa Vite o React con compilación, publica el contenido de la carpeta `dist` (en Vite pon `base: './'` para que las rutas sean relativas).
- Para varias pantallas en una sola página usa rutas con `#` (`#/ventas`, `#/gastos`): Publica con IA sirve los archivos tal cual y no reescribe rutas.
- Tipos de archivo permitidos: HTML, CSS, JS, JSON, TXT, MD, CSV, XML, SVG, imágenes (PNG, JPG, GIF, WebP, AVIF, ICO), fuentes (WOFF, WOFF2, TTF, OTF), audio y video (MP3, WAV, MP4, WebM), PDF y WASM.
- Por versión: hasta 500 archivos, 10 MB por archivo y 25 MB en total. Por el conector manda máximo unos 3 MB por llamada; para sitios más grandes usa la API (sección 9) o GitHub.

## 2. Reglas del marco aislado (muy importante)

La página corre dentro de un marco aislado (un iframe con sandbox y origen opaco), igual que los artifacts de Claude o ChatGPT. Por eso:

- **No hay localStorage, sessionStorage, cookies ni IndexedDB.** Si los usas, el navegador lanza un error de seguridad y la página se rompe. Guarda el estado en memoria (variables de JavaScript) o en las tablas de Publica con IA.
- Sí funcionan: scripts, formularios, `alert` y `confirm`, ventanas nuevas (`window.open` o links con `target="_blank"`) y descargas (por ejemplo, exportar un CSV con un Blob).
- `fetch` a otras APIs públicas funciona solo si esa API permite CORS para cualquier origen.
- **Nunca pongas llaves, contraseñas ni tokens en el código:** cualquiera que abra la página los puede ver.
- No intentes leer `window.parent`, `document.cookie` ni la URL de la página exterior.
- Los errores de JavaScript de la página se registran solos: revísalos con la herramienta ver_errores.

## 3. Datos con publica.js

Los datos viven en las tablas del espacio de la persona (se crean y cargan con crear_tabla y cargar_filas). La página los lee con la librería de Publica con IA. Ponla antes de tu código:

~~~html
<script src="https://publica.sumaconia.com/publica.js"></script>
~~~

Uso básico:

~~~js
await publica.listo();                                   // espera el permiso de la página; llámalo una vez al inicio
const filas = await publica.tabla('ventas').obtener();  // [{ _id, fecha, sucursal, monto, ... }]
~~~

### Consultas: publica.tabla(nombre)

`publica.tabla('nombre')` arma una consulta. Encadena los pasos que necesites y termina con `.obtener()`, que regresa una promesa con una lista de objetos. Publica con IA hace las cuentas en el servidor: no bajes toda la tabla para sumar en el navegador.

| Paso | Qué hace | Ejemplo |
|---|---|---|
| `.filtrar(columna, op, valor)` | Solo las filas que cumplen. `op`: `=` `!=` `>` `>=` `<` `<=` `contiene` `en`. Con dos datos, `.filtrar(columna, valor)` es `=` | `.filtrar('sucursal', '=', 'Norte')` |
| `.agrupar(columna, { por?, como? })` | Agrupa. En fechas, `por` puede ser `'dia'`, `'semana'`, `'mes'` (AAAA-MM) o `'anio'`; `como` cambia el nombre del campo. El segundo dato es un objeto, no un texto | `.agrupar('fecha', { por: 'mes' })` |
| `.sumar(columna, como?)` | Suma (solo número o dinero) | `.sumar('monto', 'total')` |
| `.promedio(columna, como?)` | Promedio (solo número o dinero) | `.promedio('monto', 'ticket')` |
| `.contar(como?)` | Cuántas filas | `.contar('pedidos')` |
| `.minimo(columna, como?)` / `.maximo(columna, como?)` | Mínimo y máximo | `.maximo('fecha', 'ultima')` |
| `.ordenar(columna, dir?)` | Ordena por una columna o por el nombre (`como`) de una agrupación o métrica. `dir`: `'asc'` o `'desc'` | `.ordenar('total', 'desc')` |
| `.limite(n)` / `.desplazar(n)` | Cuántos resultados (hasta 5,000) y cuántos saltar | `.limite(10)` |
| `.obtener()` | Ejecuta la consulta | |

- Sin agrupar ni métricas regresa las filas, cada una con su `_id`.
- Con métricas, cada resultado trae las columnas agrupadas y las métricas con su nombre (`como`). Si no pones `como`, el nombre es `suma_monto`, `promedio_monto`, `conteo`, etc.
- Al agrupar una fecha por mes, el valor es un texto `'2026-03'`; por día, `'2026-03-15'`; por año, `'2026'`.
- Los números llegan como números de JavaScript; las fechas, como texto `AAAA-MM-DD`.

### Otras funciones

- `await publica.tablas()`: las tablas del espacio con sus columnas y cuántas filas tienen.
- `await publica.agregar('tabla', fila)`: agrega una fila (o una lista de hasta 100) desde la página y regresa cuántas agregó, por ejemplo un formulario. Solo funciona si el proyecto permite escribir en esa tabla (herramienta editar_proyecto, campo tablas_escritura) y el acceso es "equipo" o "contrasena". Valida los datos antes de mandarlos.
- `await publica.usuario()`: quién ve la página, `{ correo }`, en proyectos de modo equipo; en público o con contraseña regresa `null`.
- Atajo: `await publica.tabla('ventas').filtrar('sucursal', 'Norte')` funciona sin `.obtener()` (la consulta se puede esperar directo).

publica.js solo funciona dentro del proyecto publicado en Publica con IA (en su link), no al abrir el archivo en la computadora. Si una llamada falla, la promesa se rechaza con un `Error` cuyo mensaje explica qué pasó (en español): muéstralo en la página en lugar de dejarla en blanco.

## 4. Ejemplo completo: dashboard de ventas

Supone una tabla `ventas` con columnas `fecha` (fecha), `sucursal` (texto), `producto` (texto) y `monto` (dinero). Tiene tarjetas de indicadores, una gráfica de línea por mes con Chart.js y una tabla con las últimas ventas.

**index.html**

~~~html
<!doctype html>
<html lang="es-MX">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Ventas 2026</title>
  <link rel="stylesheet" href="estilos.css">
  <script src="https://publica.sumaconia.com/publica.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
</head>
<body>
  <main>
    <header>
      <h1>Ventas 2026</h1>
      <p id="estado" role="status">Cargando datos…</p>
    </header>

    <section class="kpis" aria-label="Indicadores">
      <article class="kpi"><h2>Ventas totales</h2><p id="kpi-total">…</p></article>
      <article class="kpi"><h2>Pedidos</h2><p id="kpi-pedidos">…</p></article>
      <article class="kpi"><h2>Ticket promedio</h2><p id="kpi-ticket">…</p></article>
    </section>

    <section class="panel">
      <h2>Ventas por mes</h2>
      <div class="grafica">
        <canvas id="grafica-meses" role="img" aria-label="Gráfica de ventas por mes"></canvas>
      </div>
    </section>

    <section class="panel">
      <h2>Últimas ventas</h2>
      <div class="tabla-scroll">
        <table>
          <thead><tr><th scope="col">Fecha</th><th scope="col">Sucursal</th><th scope="col">Producto</th><th scope="col" class="num">Monto</th></tr></thead>
          <tbody id="ultimas"></tbody>
        </table>
      </div>
    </section>
  </main>
  <script src="app.js"></script>
</body>
</html>
~~~

**app.js**

~~~js
const pesos = new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' });
const enteros = new Intl.NumberFormat('es-MX');
const estado = document.getElementById('estado');

async function cargar() {
  await publica.listo();
  const [totales, porMes, ultimas] = await Promise.all([
    publica.tabla('ventas').sumar('monto', 'total').contar('pedidos').promedio('monto', 'ticket').obtener(),
    publica.tabla('ventas').agrupar('fecha', { por: 'mes' }).sumar('monto', 'total').ordenar('fecha').obtener(),
    publica.tabla('ventas').ordenar('fecha', 'desc').limite(10).obtener(),
  ]);

  const t = totales[0] || { total: 0, pedidos: 0, ticket: 0 };
  document.getElementById('kpi-total').textContent = pesos.format(t.total || 0);
  document.getElementById('kpi-pedidos').textContent = enteros.format(t.pedidos || 0);
  document.getElementById('kpi-ticket').textContent = pesos.format(t.ticket || 0);

  new Chart(document.getElementById('grafica-meses'), {
    type: 'line',
    data: {
      labels: porMes.map((m) => m.fecha),
      datasets: [{ label: 'Ventas', data: porMes.map((m) => m.total), borderColor: '#1766ea', backgroundColor: 'rgba(23,102,234,0.12)', fill: true, tension: 0.3 }],
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      plugins: { legend: { display: false }, tooltip: { callbacks: { label: (c) => pesos.format(c.parsed.y) } } },
      scales: { y: { ticks: { callback: (v) => pesos.format(v) } } },
    },
  });

  const cuerpo = document.getElementById('ultimas');
  for (const v of ultimas) {
    const fila = document.createElement('tr');
    for (const [valor, clase] of [[v.fecha, ''], [v.sucursal, ''], [v.producto, ''], [pesos.format(v.monto || 0), 'num']]) {
      const celda = document.createElement('td');
      celda.textContent = valor ?? '';   // textContent, nunca innerHTML con datos
      if (clase) celda.className = clase;
      fila.appendChild(celda);
    }
    cuerpo.appendChild(fila);
  }
  estado.textContent = 'Actualizado: ' + new Date().toLocaleString('es-MX');
}

cargar().catch((error) => {
  estado.textContent = 'No se pudieron cargar los datos: ' + error.message;
});
~~~

**estilos.css**

~~~css
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; font-size: 16px; color: #0f2144; background: #f5f7fb; }
main { max-width: 1100px; margin: 0 auto; padding: 24px 16px 48px; display: grid; gap: 20px; }
h1 { margin: 0; font-size: 1.75rem; }
h2 { margin: 0 0 12px; font-size: 1rem; color: #5b6478; }
.kpis { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.kpi, .panel { background: #fff; border-radius: 16px; padding: 16px; box-shadow: 0 1px 2px rgba(15,33,68,.06); }
.kpi p { margin: 0; font-size: 1.75rem; font-weight: 700; }
.grafica { position: relative; height: 300px; }
.tabla-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px 8px; border-bottom: 1px solid #dee5f0; text-align: left; white-space: nowrap; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
~~~

## 5. Tipos de columna

| Tipo | Para qué | Cómo se carga |
|---|---|---|
| `texto` | Nombres, categorías, notas | Cualquier texto (hasta 4,000 caracteres) |
| `numero` | Cantidades, piezas, porcentajes | `1250.5` (también acepta `"1,250.50"`) |
| `dinero` | Montos | `1250.5` (también acepta `"$1,250.50"`). Se guarda como número; dale formato en la página |
| `fecha` | Días | `"2026-03-15"` (también acepta `"15/03/2026"`) |
| `booleano` | Sí o no | `true`/`false` (también `"si"`, `"no"`, `1`, `0`) |

- Nombres de tabla y columna: minúsculas, números y guion bajo, empezando con letra (`ventas_2026`, `fecha_venta`).
- Sumar y promediar solo funciona en `numero` y `dinero`; agrupar por día, semana, mes o año, solo en `fecha`.
- Un valor vacío (`""` o `null`) se guarda como vacío.

## 6. Ejemplos de consultas

~~~js
// Ventas por sucursal, de mayor a menor
await publica.tabla('ventas').agrupar('sucursal').sumar('monto', 'total').ordenar('total', 'desc').obtener();

// Ventas por mes y sucursal en el primer trimestre
await publica.tabla('ventas')
  .filtrar('fecha', '>=', '2026-01-01').filtrar('fecha', '<=', '2026-03-31')
  .agrupar('fecha', { por: 'mes' }).agrupar('sucursal')
  .sumar('monto', 'total').ordenar('fecha').obtener();

// Top 5 productos por número de pedidos
await publica.tabla('ventas').agrupar('producto').contar('pedidos').ordenar('pedidos', 'desc').limite(5).obtener();

// Varias sucursales a la vez y búsqueda de texto
await publica.tabla('ventas').filtrar('sucursal', 'en', ['Norte', 'Centro']).filtrar('producto', 'contiene', 'café').obtener();

// Paginar filas de 50 en 50
await publica.tabla('ventas').ordenar('fecha', 'desc').limite(50).desplazar(100).obtener();
~~~

Con la herramienta consultar del conector se hacen las mismas consultas, en JSON:

~~~json
{
  "tabla": "ventas",
  "filtros": [{ "columna": "fecha", "op": ">=", "valor": "2026-01-01" }],
  "agrupar": [{ "columna": "fecha", "por": "mes" }],
  "metricas": [{ "fn": "suma", "columna": "monto", "como": "total" }],
  "orden": [{ "columna": "fecha", "dir": "asc" }]
}
~~~

## 7. Verifica las cifras y nunca inventes datos

- Usa solo datos reales que te dio la persona o que leíste de sus archivos. Si faltan datos, pregunta. Si haces un ejemplo, dilo claramente en el chat y en la página.
- Antes de decir una cifra o de publicar, corre la misma consulta con la herramienta consultar y compárala con lo que muestra la página (totales, número de filas, meses).
- Después de cargar datos, revisa que el número de filas y la suma total coincidan con el archivo original.
- No dejes datos fijos en el HTML si van a cambiar: cárgalos a una tabla y léelos con publica.js. Así la persona solo actualiza la tabla y el dashboard cambia solo.
- Si la persona dice que algo no se ve, revisa ver_errores antes de cambiar el código.

## 8. Accesibilidad y celular

- `<html lang="es-MX">`, `<meta name="viewport" content="width=device-width, initial-scale=1">` y un `<title>` claro.
- Diseña primero para celular: una columna, tarjetas que se acomodan con `grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))` y tablas dentro de un contenedor con `overflow-x: auto`.
- Texto de 16 px o más y buen contraste (texto oscuro sobre fondo claro, o al revés).
- Botones con `<button>`, campos con `<label>`, títulos en orden (`h1`, `h2`) y un foco visible al usar el teclado.
- A cada gráfica ponle `role="img"` y un `aria-label` que diga qué muestra, y deja también los números en una tabla o en texto.
- No dependas solo del color para dar información (agrega texto o íconos).
- Números y fechas con formato de México: `Intl.NumberFormat('es-MX')` y `toLocaleDateString('es-MX')`.
- Muestra un mensaje mientras carga y otro claro si falla.
- Inserta los datos con `textContent`, no con `innerHTML`.

## 9. Límites del plan

| Qué | Límite |
|---|---|
| Proyectos | 5 |
| Tablas | 30 |
| Filas en todo el espacio | 200,000 |
| Filas por carga | 5,000 |
| Resultados por consulta | 5,000 |
| Publicado (todas las versiones) | 1 GB |
| Personas invitadas por proyecto | 50 |
| Llaves de API activas | 10 |
| Desde la página (publica.agregar) | 100 filas por llamada |

Para publicar, el espacio debe tener la suscripción activa. Si no lo está, la persona la activa en https://publica.sumaconia.com/consola. Revisa el uso con estado_de_mi_espacio.

## 10. API con llave (Claude Code, scripts y GitHub Actions)

Para publicar o cargar datos sin el chat, crea una llave con la herramienta crear_llave_api (o en https://publica.sumaconia.com/consola). Se ve una sola vez: guárdala como secreto, nunca en el código de una página ni en un repositorio. Todas las rutas reciben y regresan JSON y piden `Authorization: Bearer pc_...`. Los errores llegan como `{ "error": "codigo", "mensaje": "..." }`.

| Método y ruta | Qué hace |
|---|---|
| `GET /api/v1/proyectos` | Lista los proyectos |
| `POST /api/v1/proyectos` | Crea un proyecto: `{ slug, titulo, descripcion?, acceso? }` |
| `GET /api/v1/proyectos/<slug>/versiones` | Historial de versiones |
| `POST /api/v1/proyectos/<slug>/versiones` | Publica una versión: JSON o archivos (multipart) |
| `GET /api/v1/tablas` | Lista las tablas con sus columnas |
| `POST /api/v1/tablas` | Crea una tabla: `{ nombre, titulo?, columnas: [{ nombre, tipo, titulo? }] }` |
| `POST /api/v1/tablas/<nombre>/filas` | Carga filas: `{ filas: [...], reemplazar? }` |
| `POST /api/v1/tablas/<nombre>/consultar` | Consulta: `{ filtros?, agrupar?, metricas?, orden?, limite?, desplazar? }` |

Publicar con JSON (`codificacion`: `"texto"` o `"base64"`):

~~~bash
curl -X POST "https://publica.sumaconia.com/api/v1/proyectos/ventas-2026/versiones" \
  -H "Authorization: Bearer $LIENZO_LLAVE" -H "Content-Type: application/json" \
  -d '{"archivos":[{"ruta":"index.html","contenido":"<!doctype html><h1>Hola</h1>"}],"nota":"Primera versión"}'
~~~

Publicar una carpeta (por ejemplo `dist`) con archivos: un campo `archivo` por archivo y su `ruta` en el mismo orden. Campos opcionales: `nota`, `publicar` (`false` para guardarla sin mostrarla), `partir_de_version`.

~~~bash
cd dist
args=()
while IFS= read -r -d '' f; do
  args+=(-F "archivo=@$f" --form-string "ruta=${f#./}")
done < <(find . -type f -print0)
curl -fsS -X POST "https://publica.sumaconia.com/api/v1/proyectos/ventas-2026/versiones" \
  -H "Authorization: Bearer $LIENZO_LLAVE" "${args[@]}" --form-string "nota=Desde mi computadora"
~~~

Cada petición puede pesar hasta unos 4 MB. Para un sitio más grande, súbelo en partes: la primera con `index.html` y `publicar=false`, y las siguientes con `partir_de_version=<número que regresó la anterior>` (copia lo ya subido) y `publicar=true` en la última. O conecta el proyecto a GitHub desde la consola.

Cargar filas y consultar:

~~~bash
curl -X POST "https://publica.sumaconia.com/api/v1/tablas/ventas/filas" \
  -H "Authorization: Bearer $LIENZO_LLAVE" -H "Content-Type: application/json" \
  -d '{"filas":[{"fecha":"2026-03-01","sucursal":"Norte","producto":"Café","monto":120.5}],"reemplazar":false}'

curl -X POST "https://publica.sumaconia.com/api/v1/tablas/ventas/consultar" \
  -H "Authorization: Bearer $LIENZO_LLAVE" -H "Content-Type: application/json" \
  -d '{"agrupar":[{"columna":"fecha","por":"mes"}],"metricas":[{"fn":"suma","columna":"monto","como":"total"}]}'
~~~

GitHub Actions (compila con Vite y publica `dist` en cada cambio a main). Guarda la llave en Settings > Secrets and variables > Actions como `LIENZO_LLAVE`:

~~~yaml
name: Publicar en Publica con IA
on:
  push:
    branches: [main]
jobs:
  publicar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci && npm run build
      - name: Publicar dist
        env:
          LIENZO_LLAVE: ${{ secrets.LIENZO_LLAVE }}
        run: |
          cd dist
          args=()
          while IFS= read -r -d '' f; do
            args+=(-F "archivo=@$f" --form-string "ruta=${f#./}")
          done < <(find . -type f -print0)
          curl -fsS -X POST "https://publica.sumaconia.com/api/v1/proyectos/ventas-2026/versiones" \
            -H "Authorization: Bearer $LIENZO_LLAVE" "${args[@]}" \
            --form-string "nota=Commit ${GITHUB_SHA::7}"
~~~

## 11. Antes de publicar, revisa

1. `index.html` está en la raíz y todas las rutas son relativas.
2. No usas localStorage, sessionStorage, cookies ni IndexedDB.
3. Cargas `https://publica.sumaconia.com/publica.js` y llamas `await publica.listo()` antes de leer datos.
4. Las cifras coinciden con la herramienta consultar.
5. Se ve bien en celular y tiene mensajes de carga y de error.
6. No hay llaves ni contraseñas en el código.
7. Después de publicar, dale el link a la persona y dile quién lo puede ver (público, equipo o contraseña).
