# Guía del tema en código de Hystore

Un tema en código de Hystore es una carpeta de archivos Liquid, CSS y JSON que
dibuja la vitrina de la tienda: cabecera, pie, inicio, tienda, páginas de
producto, noticias, jugadores. El código de la propia tienda se encarga del
carrito, el pago, la cuenta y el inicio de sesión, que nunca cargan el tema.

Esta guía es la referencia para personas y asistentes de IA que editan un
tema. Todo aquí lo revisan los servidores de Hystore: un archivo que rompe una
regla se rechaza al guardar, con el archivo, la línea y el motivo.

## Flujo de trabajo

```
npx @hystudios/hystore login            # aprueba en el panel (sin token para pegar)
npx @hystudios/hystore theme pull       # descarga el borrador a esta carpeta
npx @hystudios/hystore theme dev        # sigue la carpeta, envía cada guardado, vista previa en vivo
npx @hystudios/hystore theme check      # revisa en tu máquina, sin enviar
npx @hystudios/hystore theme push       # envía los cambios al borrador
npx @hystudios/hystore theme publish    # pide publicar; quien tiene permiso aprueba en el panel
npx @hystudios/hystore theme versions   # las últimas 30 versiones publicadas
npx @hystudios/hystore theme rollback 3 # trae la versión 3 de vuelta al borrador
npx @hystudios/hystore theme new        # empieza una carpeta a partir del tema Base de Hystore
```

- Las ediciones siempre van al **borrador**. La tienda en el aire solo cambia
  cuando se aprueba un pedido de publicación en el panel (Personalización >
  Tema en código).
- El enlace de vista previa (de `theme dev`) muestra el borrador en la
  dirección real de la tienda durante 24 horas y se recarga solo tras cada
  envío.
- Si alguien cambió un archivo en el panel después de tu último pull, el envío
  de ese archivo se rechaza como conflicto: haz pull, une y envía de nuevo.

## Reglas fijas

Esto se rechaza (el archivo no se guarda):

- `<script>`, `<style>`, `<iframe>`, `<form>`, `<input>`, `<textarea>`,
  `<select>`, `<object>`, `<embed>`, `<base>`, `<meta>`, `<link>`,
  `<foreignObject>` de SVG.
- Atributos de evento (`onclick`, `onload`, ...), enlaces `javascript:`,
  `vbscript:` y `data:` (excepto `data:image/png|jpeg|gif|webp;base64`),
  `srcdoc`, `http-equiv`, referencias numéricas de carácter (`&#...;`):
  escribe el carácter mismo.
- En CSS: `@import`, `expression()`, `behavior`, `-moz-binding`,
  `@namespace`, `@document`, `image-set()`, escapes con barra invertida fuera
  de strings, y `url()` que no sea `assets/<nombre>`, un archivo
  `https://fonts.gstatic.com/...` o una imagen pequeña
  `data:image/...;base64`. La clase `hystore-required` está reservada.
- Las tags Liquid `include`, `layout` y `block` (usa `render`, el archivo de
  layout y `hystore_block`).

No hay JavaScript en las páginas del tema. La interacción viene de atributos
`data-hystore-*` y de elementos `<hystore-*>` (mira Piezas interactivas). La
tienda limpia de nuevo el HTML dibujado, y su política de seguridad de
contenido bloquea scripts. Lo que necesita JavaScript va en una app aislada
(mira Apps).

Un tema solo lee datos públicos de la tienda y el nick y la skin de quien
mira. Nunca ve saldos, correos, pedidos ni pagos, y no cambia precios: la
compra pasa por `data-hystore-add-to-cart`, que maneja la tienda.

## Carpeta

```
theme.json                 nombre, modos de color, configuración general (obligatorio)
layout/theme.liquid        marco de todas las páginas; debe mostrar {{ content }} (obligatorio)
pages/<página>.liquid      un archivo por página (pages/home.liquid es obligatorio)
templates/<página>.json    secciones de una página, en orden, editables en "Personalizar tema"
sections/<nombre>.liquid   bloques con un {% schema %}, usados por templates o {% section %}
components/<nombre>.liquid piezas usadas con {% render 'nombre' %}
styles/<nombre>.css        se cargan todos los archivos, en orden de nombre
locales/pt.json en.json es.json   textos del filtro | t
config/values.json         valores elegidos en "Personalizar tema"
assets/<nombre>.<ext>      imágenes (png, jpg, webp, gif, avif hasta 3 MB) y fuentes (woff, woff2 hasta 1 MB)
apps/<app>/<archivo>       apps aisladas con JavaScript: html, js, css, json (mira Apps)
```

- Nombres: letras minúsculas, números, `-` y `_`, un nivel dentro de cada
  carpeta (`sections/hero.liquid`, no `sections/home/hero.liquid`); las apps
  usan dos (`apps/ruleta/app.js`).
- Límites: 400 archivos de texto, 256 KB por archivo, 5 MB de texto; 100
  archivos de medios, 40 MB. Los archivos SVG no entran como medios (`<svg>`
  directo en el Liquid sí).
- Páginas: `home`, `shop`, `category`, `product`, `search`, `news`, `post`,
  `page`, `players`, `404`, `maintenance`. Una página que el tema no tiene la
  dibuja el diseño clásico de la tienda.

| Página | Dirección | Objetos extra |
|---|---|---|
| home | `/` | |
| shop | `/loja`, `/loja/<servidor>` | `categories`, `server` (con servidor) |
| category | `/loja/<servidor>/<categoría>` | `category`, `products` (48) |
| product | `/loja/<servidor>/<categoría>/produto/<slug>` | `product` |
| search | `/busca?q=...` | `search.query`, `search.results` (48 productos) |
| news | `/noticias` | |
| post | `/noticias/<slug>` | `post` |
| page | `/p/<slug>` | `page.slug` |
| players | `/jogadores` | |
| 404, maintenance | | |

Carrito (`/carrinho`), pago, perfil (`/perfil`), compras, contraseña e inicio
de sesión (`/entrar`) son páginas de la propia tienda: enlázalas, nunca las
rehagas.

## theme.json

```json
{
  "name": "Mi tema",
  "version": "1.0.0",
  "author": "Yo",
  "color_modes": ["dark", "light"],
  "settings": [
    { "id": "accent", "type": "color", "label": "Color de acento", "default": "" },
    { "id": "radius", "type": "range", "label": "Redondeo", "min": 0, "max": 28, "default": 14 },
    { "id": "show_search", "type": "checkbox", "label": "Búsqueda en la cabecera", "default": true }
  ]
}
```

- `color_modes`: los modos que dibuja el tema (`dark`, `light`); oscuro si
  falta. En modo claro la página no tiene la clase `dark` en el `<html>`: da
  estilo con `html:not(.dark) ...` o `html.dark ...`.
- Tipos de configuración: `text`, `textarea`, `richtext`, `image`, `url`,
  `color`, `number`, `range` (`min`, `max`, `step`), `checkbox`, `select`
  (`options`: textos o `{ "value", "label" }`), `product`, `category`,
  `server`, `list` (líneas de texto). Hasta 60 configuraciones, `id` en
  minúsculas.
- La configuración general es `{{ settings.<id> }}` en cualquier lugar, y los
  colores y números también son variables CSS `--theme-<id>` en la raíz del
  tema.
- El color primario de la tienda es `hsl(var(--primary))` (texto sobre él:
  `hsl(var(--primary-foreground))`).

`config/values.json` guarda lo que eligió el dueño de la tienda:
`{ "settings": { "accent": "#ff5500" } }`. Mantén ese formato al editarlo.

## Layout y páginas

`layout/theme.liquid` envuelve todas las páginas y debe tener `{{ content }}`
donde va la página. La tienda agrega un área obligatoria después del tema
(anuncios en los planes gratuitos, enlaces legales); no se puede ocultar ni
estilizar.

Una página es `pages/<página>.liquid`, `templates/<página>.json` o ambos. Con
ambos, el archivo de la página coloca las secciones del template con
`{{ template_sections }}`.

```liquid
{%- comment -%} pages/home.liquid {%- endcomment -%}
<div class="intro">{{ 'home.welcome' | t }}</div>
{{ template_sections }}
```

## Secciones

Una sección es `sections/<nombre>.liquid`, que termina con un
`{% schema %}`:

```liquid
<section class="hero">
  <h1>{{ section.settings.title | default: store.name }}</h1>
  {%- for block in section.blocks -%}
    <p>{{ block.settings.text }}</p>
  {%- endfor -%}
</section>
{% schema %}
{
  "name": "Destacado",
  "settings": [
    { "id": "title", "type": "text", "label": "Título", "default": "" }
  ],
  "blocks": [
    { "type": "line", "name": "Línea", "settings": [{ "id": "text", "type": "text", "label": "Texto" }] }
  ]
}
{% endschema %}
```

- `section` es `{ id, type, settings, blocks }`; la configuración viene con
  sus valores por defecto. Una sección ve lo que ve su página (`product`,
  datos cargados...).
- Un template lista secciones (como máximo 40 dibujadas por página):

```json
{
  "sections": {
    "hero": { "type": "hero", "settings": { "title": "Bienvenido" } },
    "news": { "type": "latest-news", "disabled": true }
  },
  "order": ["hero", "news"]
}
```

  `type` es el nombre del archivo en `sections/`; `"disabled": true` la oculta.
- `{% section 'hero' %}` dibuja una sección en un lugar fijo (layout, página);
  los valores vienen de `config/values.json` en `sections.hero.settings`.

## Componentes

`components/<nombre>.liquid`, usados con `{% render 'nombre', clave: valor %}`.
Un componente solo ve lo que recibe (más las variables globales).

```liquid
{% render 'product-card', product: item %}
```

## Textos en tres idiomas

`locales/pt.json`, `en.json` y `es.json` guardan textos (se permiten objetos
anidados). `{{ 'nav.shop' | t }}` muestra el texto en el idioma de quien
visita, cayendo en el portugués y después en la propia clave. Un archivo de
idioma que falta es un aviso.

## Datos

En todas partes:

| Objeto | Campos |
|---|---|
| `store` | `id`, `name`, `slogan`, `description`, `logo`, `ip`, `type` (`game` o `digital`), `game` (`minecraft`, `hytale` o null), `currency` |
| `route` | `path`, `page`, `params` (`server_id`, `category_slug`, `product_slug`, `slug`), `query.q` |
| `modules` | `forum`, `wiki`, `support`, `forms`, `launcher`, `discord`, `vip` (true cuando está activo) |
| `customer` | `logged_in`, `nick`, `skin_url` |
| `cart` | `count` |
| `settings` | configuración general del `theme.json` |
| `locale` | `pt`, `en` o `es` |
| `color_mode` | `dark` o `light` |

Más datos, cuando hagan falta, con `{% load %}` (como máximo 24 por página; la
misma carga se busca una sola vez):

```liquid
{% load products, limit: 8, featured: true as featured %}
{% for p in featured %}{% render 'product-card', product: p %}{% endfor %}
```

| Load | Opciones | Trae |
|---|---|---|
| `products` | `limit` (12), `category`, `server`, `featured`, `query` | lista de productos |
| `product` | `slug`, `server` | un producto o null |
| `categories` | `server` | lista de categorías |
| `servers` | | lista de `{ id, name, url, image, online }` |
| `server_status` | | `{ online, players_online, players_max, version }` o null |
| `news` | `limit` (6), `page` | lista de posts |
| `post` | `slug` | un post o null |
| `ranking` | `period` (`month` o `all`), `limit` (5) | lista de `{ position, nick, total_spent, orders, skin_url }` |
| `top_supporter` | | `{ nick, total_spent, purchases, skin_url }` o null |
| `goal` | | `{ target, current, percent, ends_at, rewards }` o null |
| `discord` | | `{ url, server_id, widget }` |
| `launcher` | | `{ name, version, downloads }` o null |
| `socials` | | mapa `instagram`, `discord`, `twitter`, `tiktok`, `telegram`, `whatsapp` → URL |
| `online_players` | | `{ total, servers: [{ name, count, max, players }] }` |

- Producto: `id`, `name`, `slug`, `url`, `price`, `original_price`,
  `on_sale`, `image`, `short_description`, `description` (HTML),
  `duration_days`, `min_quantity`, `featured`, `sold_out`, `digital`,
  `category_slug`, `server_id`.
- Categoría: `id`, `name`, `slug`, `url`, `description`, `image`,
  `product_count`.
- Post: `id`, `title`, `slug`, `url`, `author`, `image`, `excerpt`, `content`
  (HTML), `published_at`.

## Filtros

De Hystore:

- `t`: texto en el idioma de quien visita, `{{ 'cart.add' | t }}`.
- `money`: precio en la moneda de la tienda, `{{ product.price | money }}`.
- `asset_url`: dirección de un medio enviado, `{{ 'logo.webp' | asset_url }}`.
- `skin_url`: cabeza de Minecraft de un nick, `{{ customer.nick | skin_url: 64 }}`.
- `raw`: solo para HTML que la tienda ya limpió (`product.description`,
  `post.content`). Todo lo demás se escapa.

De Liquid: `abs`, `append`, `at_least`, `at_most`, `capitalize`, `ceil`,
`compact`, `concat`, `date`, `default`, `divided_by`, `downcase`, `escape`,
`escape_once`, `first`, `floor`, `join`, `json`, `last`, `lstrip`, `map`,
`minus`, `modulo`, `newline_to_br`, `plus`, `prepend`, `remove`,
`remove_first`, `replace`, `replace_first`, `reverse`, `round`, `rstrip`,
`size`, `slice`, `sort`, `sort_natural`, `split`, `strip`, `strip_html`,
`strip_newlines`, `sum`, `times`, `truncate`, `truncatewords`, `uniq`,
`upcase`, `url_decode`, `url_encode`, `where`.

Tags: `if`/`elsif`/`else`, `unless`, `case`/`when`, `for` (`break`,
`continue`), `assign`, `capture`, `echo`, `liquid`, `comment`, `raw`,
`cycle`, `increment`, `decrement`, `tablerow`, más `render`, `section`,
`load`, `hystore_block`, `schema`.

## Bloques listos

`{% hystore_block 'nombre', clave: valor %}` coloca una pieza de la tienda que
necesita datos en vivo o la lógica de la tienda:

`heading`, `image`, `button`, `icon`, `video`, `card`, `testimonial`,
`countdown`, `banner`, `spacer`, `divider`, `server-hero`, `products`,
`featured-product`, `categories`, `servers`, `ranking`, `top-supporter`,
`goal`, `news`, `discord`, `launcher`, `server-status`, `server-ip`,
`socials`, `logo`, `menu`, `account-actions`, `language`, `color-mode`,
`legal-links`, `copyright`.

El nombre va entre comillas. Las opciones son texto, números o true/false.

## Piezas interactivas

Atributos (la tienda hace el trabajo, sin JavaScript en el tema):

| Atributo | Qué hace |
|---|---|
| `data-hystore-add-to-cart="<id del producto>"` | agrega el producto al carrito (pide iniciar sesión antes) |
| `data-hystore-open="cart\|account\|login\|search"` | abre ese lugar |
| `data-hystore-copy="texto"` | copia el texto (IP del servidor...) |
| `data-hystore-color-mode="toggle\|dark\|light"` | cambia el modo de color |
| `data-hystore-language="pt\|en\|es"` | cambia el idioma |
| `data-hystore-cart-count` | el elemento siempre muestra la cantidad del carrito |
| `data-hystore-tabs` | los botones `[data-hystore-tab="x"]` muestran `[data-hystore-panel="x"]` |
| `data-hystore-accordion` (`="single"` deja uno abierto) | cada `[data-hystore-accordion-item]` tiene un `[data-hystore-accordion-toggle]` y un `[data-hystore-accordion-panel]` |
| `data-hystore-modal="id"` | abre `#id`, que tiene `data-hystore-modal-panel`; `data-hystore-modal-close` cierra |
| `data-hystore-carousel` | `[data-hystore-slides]` se desplaza, `[data-hystore-prev]`/`[data-hystore-next]`, `data-hystore-autoplay="5000"` |
| `data-hystore-countdown="2026-12-24T23:59:00Z"` | llena `[data-hystore-days]`, `[data-hystore-hours]`, `[data-hystore-minutes]`, `[data-hystore-seconds]` (o su propio texto) |
| `data-hystore-animate` | recibe `data-visible` cuando aparece en pantalla (anímalo en CSS) |

Elementos que llena la tienda: `<hystore-cart-count>`, `<hystore-account>`
(iniciar sesión o el menú de la cuenta; con `data-cart` muestra también el
botón del carrito de la tienda), `<hystore-server-status>`,
`<hystore-online-players>`, `<hystore-search data-placeholder="...">`,
`<hystore-skin data-nick="Steve" data-size="128">`,
`<hystore-video data-youtube="<id o enlace>">`,
`<hystore-app data-app="<nombre>">` (una app aislada, mira Apps).

Los enlaces dentro de la tienda (`<a href="/loja">`) cambian de página sin
recargar; las páginas de cuenta y pago siempre abren como página nueva.

## Imágenes y fuentes

Ponlas en `assets/` (la herramienta las envía; el servidor revisa los bytes).

```liquid
<img src="{{ 'banner.webp' | asset_url }}" alt="{{ store.name }}">
```

```css
@font-face { font-family: "Display"; src: url(assets/display.woff2) format("woff2"); font-display: swap; }
.hero { background-image: url(assets/banner.webp); }
```

## Apps

Una app es una pequeña página con JavaScript (calculadora, ruleta de premios,
minijuego, animación) que el tema coloca con
`<hystore-app data-app="ruleta"></hystore-app>`. Vive en `apps/<app>/` y
empieza por `index.html`:

```
apps/ruleta/index.html   la página (obligatoria)
apps/ruleta/app.js       el script
apps/ruleta/app.css      el estilo
apps/ruleta/app.json     { "name": "Ruleta de premios", "height": 420, "permissions": ["customer"] }
```

Corre aislada, en un marco en otro dominio (`hystoreapps.cloud`): sin red
(`fetch`, XHR y WebSocket bloqueados), sin cookies ni almacenamiento, sin
formularios, sin ventanas nuevas, y no puede cambiar ni navegar la página de la
tienda. Habla con la tienda solo por el puente:

```html
<script src="/sdk/v1.js"></script>
<script src="app.js"></script>
```

```js
hystore.products({ category: 'vips', limit: 6 }).then(function (products) {
  // [{ id, name, url, price, priceText, originalPrice, onSale, image, shortDescription, ... }]
});
hystore.categories();                    // categorías activas
hystore.addToCart(productId, 1);         // 'added', o 'login' (la tienda pide iniciar sesión)
hystore.navigate('/loja');               // una página de la tienda
hystore.open('cart');                    // 'cart' | 'login' | 'account' | 'search'
hystore.context();                       // { locale, colorMode, store: { name, currency } }
hystore.customer();                      // { loggedIn, nick, skinUrl }, solo con "customer" en app.json
hystore.resize(600);                     // o hystore.autosize() para seguir la página
```

- Todo es dato público: el precio viene de la tienda, y el carrito recibe solo
  el id del producto. Las páginas de dinero y de cuenta nunca muestran apps.
- Carga solo los archivos de la propia app y `/sdk/v1.js`; scripts y estilos
  de otros sitios, marcos y formularios se rechazan al guardar. Las imágenes
  pueden ser imágenes `data:` o las imágenes y fuentes del tema por su
  dirección completa (`https://theme-assets.hystore.com.br/...`, como la dan
  `GET /v1/theme/assets` y la herramienta `list_assets` del MCP).
- `app.json` es opcional: `height` de 40 a 2000 píxeles (320 si falta), `name`
  para lectores de pantalla, `permissions` solo `customer`.
- Límites: 10 apps por tema, 20 archivos y 512 KB por app.
- Los mensajes de más se ignoran; agregar al carrito y cambiar de página tienen
  límites propios, pequeños.

## Antes de publicar

- `npx @hystudios/hystore theme check` sin errores.
- Todo texto que lee el visitante pasa por `| t`, en pt, en y es.
- El tema funciona en los modos de color que declara y en el celular.
- Los enlaces de carrito, cuenta e inicio de sesión apuntan a `/carrinho`,
  `/perfil` y `/entrar` (o usan `data-hystore-open`).
