# Guia do tema em código da Hystore

Um tema em código da Hystore é uma pasta de arquivos Liquid, CSS e JSON que
desenha a vitrine da loja: cabeçalho, rodapé, início, loja, páginas de
produto, notícias, jogadores. O código da própria loja cuida do carrinho, do
pagamento, da conta e do login, que nunca carregam o tema.

Este guia é a referência para pessoas e para assistentes de IA que editam um
tema. Tudo aqui é conferido pelos servidores da Hystore: um arquivo que quebra
uma regra é recusado ao salvar, com o arquivo, a linha e o motivo.

## Fluxo de trabalho

```
npx @hystudios/hystore login            # aprove no painel (sem token para colar)
npx @hystudios/hystore theme pull       # baixe o rascunho para esta pasta
npx @hystudios/hystore theme dev        # acompanhe a pasta, envie a cada gravação, prévia ao vivo
npx @hystudios/hystore theme check      # confira na sua máquina, sem enviar
npx @hystudios/hystore theme push       # envie as mudanças para o rascunho
npx @hystudios/hystore theme publish    # peça a publicação; quem tem permissão aprova no painel
npx @hystudios/hystore theme versions   # as últimas 30 versões publicadas
npx @hystudios/hystore theme rollback 3 # traga a versão 3 de volta para o rascunho
npx @hystudios/hystore theme new        # comece uma pasta a partir do tema Base da Hystore
```

- As edições vão sempre para o **rascunho**. A loja no ar só muda quando um
  pedido de publicação é aprovado no painel (Personalização > Tema em código).
- O link de prévia (do `theme dev`) mostra o rascunho no endereço real da loja
  por 24 horas e recarrega sozinho a cada envio.
- Se alguém mudou um arquivo no painel depois do seu último pull, o envio
  desse arquivo é recusado como conflito: faça o pull, junte e envie de novo.

## Regras fixas

Isto é recusado (o arquivo não é salvo):

- `<script>`, `<style>`, `<iframe>`, `<form>`, `<input>`, `<textarea>`,
  `<select>`, `<object>`, `<embed>`, `<base>`, `<meta>`, `<link>`,
  `<foreignObject>` de SVG.
- Atributos de evento (`onclick`, `onload`, ...), links `javascript:`,
  `vbscript:` e `data:` (exceto `data:image/png|jpeg|gif|webp;base64`),
  `srcdoc`, `http-equiv`, referências numéricas de caractere (`&#...;`):
  escreva o próprio caractere.
- No CSS: `@import`, `expression()`, `behavior`, `-moz-binding`,
  `@namespace`, `@document`, `image-set()`, escapes com barra invertida fora de
  strings, e `url()` que não seja `assets/<nome>`, um arquivo
  `https://fonts.gstatic.com/...` ou uma imagem pequena `data:image/...;base64`.
  A classe `hystore-required` é reservada.
- As tags Liquid `include`, `layout` e `block` (use `render`, o arquivo de
  layout e `hystore_block`).

Não existe JavaScript nas páginas do tema. A interação vem de atributos
`data-hystore-*` e de elementos `<hystore-*>` (veja Peças interativas). A loja
limpa o HTML desenhado de novo, e a política de segurança de conteúdo bloqueia
scripts. O que precisa de JavaScript vai num app isolado (veja Apps).

Um tema só lê dados públicos da loja e o nick e a skin de quem está olhando.
Ele nunca vê saldo, e-mail, pedido ou pagamento, e não muda preços: a compra
passa por `data-hystore-add-to-cart`, tratado pela loja.

## Pasta

```
theme.json                 nome, modos de cor, configurações gerais (obrigatório)
layout/theme.liquid        moldura de todas as páginas; precisa mostrar {{ content }} (obrigatório)
pages/<página>.liquid      um arquivo por página (pages/home.liquid é obrigatório)
templates/<página>.json    seções de uma página, em ordem, editáveis em "Personalizar tema"
sections/<nome>.liquid     blocos com um {% schema %}, usados por templates ou {% section %}
components/<nome>.liquid   peças usadas com {% render 'nome' %}
styles/<nome>.css          todos os arquivos são carregados, em ordem de nome
locales/pt.json en.json es.json   textos do filtro | t
config/values.json         valores escolhidos em "Personalizar tema"
assets/<nome>.<ext>        imagens (png, jpg, webp, gif, avif até 3 MB) e fontes (woff, woff2 até 1 MB)
apps/<app>/<arquivo>       apps isolados com JavaScript: html, js, css, json (veja Apps)
```

- Nomes: letras minúsculas, números, `-` e `_`, um nível dentro de cada pasta
  (`sections/hero.liquid`, não `sections/home/hero.liquid`); apps usam dois
  (`apps/roleta/app.js`).
- Limites: 400 arquivos de texto, 256 KB por arquivo, 5 MB de texto; 100
  arquivos de mídia, 40 MB. Arquivos SVG não entram como mídia (`<svg>` direto
  no Liquid pode).
- Páginas: `home`, `shop`, `category`, `product`, `search`, `news`, `post`,
  `page`, `players`, `404`, `maintenance`. Uma página que o tema não tem é
  desenhada pelo layout clássico da loja.

| Página | Endereço | Objetos a mais |
|---|---|---|
| home | `/` | |
| shop | `/loja`, `/loja/<servidor>` | `categories`, `server` (com servidor) |
| category | `/loja/<servidor>/<categoria>` | `category`, `products` (48) |
| product | `/loja/<servidor>/<categoria>/produto/<slug>` | `product` |
| search | `/busca?q=...` | `search.query`, `search.results` (48 produtos) |
| news | `/noticias` | |
| post | `/noticias/<slug>` | `post` |
| page | `/p/<slug>` | `page.slug` |
| players | `/jogadores` | |
| 404, maintenance | | |

Carrinho (`/carrinho`), pagamento, perfil (`/perfil`), compras, senha e
entrar (`/entrar`) são páginas da própria loja: aponte links para elas, nunca
as refaça.

## theme.json

```json
{
  "name": "Meu tema",
  "version": "1.0.0",
  "author": "Eu",
  "color_modes": ["dark", "light"],
  "settings": [
    { "id": "accent", "type": "color", "label": "Cor de destaque", "default": "" },
    { "id": "radius", "type": "range", "label": "Arredondamento", "min": 0, "max": 28, "default": 14 },
    { "id": "show_search", "type": "checkbox", "label": "Busca no cabeçalho", "default": true }
  ]
}
```

- `color_modes`: os modos que o tema desenha (`dark`, `light`); escuro quando
  ausente. No modo claro a página não tem a classe `dark` no `<html>`: estilize
  com `html:not(.dark) ...` ou `html.dark ...`.
- Tipos de configuração: `text`, `textarea`, `richtext`, `image`, `url`,
  `color`, `number`, `range` (`min`, `max`, `step`), `checkbox`, `select`
  (`options`: textos ou `{ "value", "label" }`), `product`, `category`,
  `server`, `list` (linhas de texto). Até 60 configurações, `id` em
  minúsculas.
- As configurações gerais são `{{ settings.<id> }}` em qualquer lugar, e cores
  e números também viram variáveis CSS `--theme-<id>` na raiz do tema.
- A cor primária da loja é `hsl(var(--primary))` (texto sobre ela:
  `hsl(var(--primary-foreground))`).

`config/values.json` guarda o que o dono da loja escolheu:
`{ "settings": { "accent": "#ff5500" } }`. Mantenha esse formato ao editá-lo.

## Layout e páginas

`layout/theme.liquid` envolve todas as páginas e precisa ter `{{ content }}`
onde a página entra. A loja põe uma área obrigatória depois do tema (anúncios
nos planos gratuitos, links legais); ela não pode ser escondida nem
estilizada.

Uma página é `pages/<página>.liquid`, `templates/<página>.json` ou os dois.
Com os dois, o arquivo da página põe as seções do template com
`{{ template_sections }}`.

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

## Seções

Uma seção é `sections/<nome>.liquid`, terminando com um `{% 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": "Destaque",
  "settings": [
    { "id": "title", "type": "text", "label": "Título", "default": "" }
  ],
  "blocks": [
    { "type": "line", "name": "Linha", "settings": [{ "id": "text", "type": "text", "label": "Texto" }] }
  ]
}
{% endschema %}
```

- `section` é `{ id, type, settings, blocks }`; as configurações vêm com os
  padrões. Uma seção vê o que a página dela vê (`product`, dados
  carregados...).
- Um template lista seções (no máximo 40 desenhadas por página):

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

  `type` é o nome do arquivo em `sections/`; `"disabled": true` esconde.
- `{% section 'hero' %}` desenha uma seção num lugar fixo (layout, página); os
  valores vêm de `config/values.json` em `sections.hero.settings`.

## Componentes

`components/<nome>.liquid`, usados com `{% render 'nome', chave: valor %}`. Um
componente vê só o que recebe (mais as variáveis globais).

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

## Textos em três línguas

`locales/pt.json`, `en.json` e `es.json` guardam textos (objetos aninhados
podem). `{{ 'nav.shop' | t }}` mostra o texto na língua de quem visita, caindo
no português e depois na própria chave. Um arquivo de língua faltando é um
aviso.

## Dados

Em todo lugar:

| Objeto | Campos |
|---|---|
| `store` | `id`, `name`, `slogan`, `description`, `logo`, `ip`, `type` (`game` ou `digital`), `game` (`minecraft`, `hytale` ou null), `currency` |
| `route` | `path`, `page`, `params` (`server_id`, `category_slug`, `product_slug`, `slug`), `query.q` |
| `modules` | `forum`, `wiki`, `support`, `forms`, `launcher`, `discord`, `vip` (true quando ligado) |
| `customer` | `logged_in`, `nick`, `skin_url` |
| `cart` | `count` |
| `settings` | configurações gerais do `theme.json` |
| `locale` | `pt`, `en` ou `es` |
| `color_mode` | `dark` ou `light` |

Mais dados, quando precisar, com `{% load %}` (no máximo 24 por página; o
mesmo carregamento é buscado uma vez só):

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

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

- Produto: `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`.
- Categoria: `id`, `name`, `slug`, `url`, `description`, `image`,
  `product_count`.
- Post: `id`, `title`, `slug`, `url`, `author`, `image`, `excerpt`, `content`
  (HTML), `published_at`.

## Filtros

Da Hystore:

- `t`: texto na língua de quem visita, `{{ 'cart.add' | t }}`.
- `money`: preço na moeda da loja, `{{ product.price | money }}`.
- `asset_url`: endereço de uma mídia enviada, `{{ 'logo.webp' | asset_url }}`.
- `skin_url`: cabeça do Minecraft de um nick, `{{ customer.nick | skin_url: 64 }}`.
- `raw`: só para HTML que a loja já limpou (`product.description`,
  `post.content`). Todo o resto é escapado.

Do 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`, mais `render`, `section`, `load`,
`hystore_block`, `schema`.

## Blocos prontos

`{% hystore_block 'nome', chave: valor %}` põe uma peça da loja que precisa
de dados ao vivo ou da lógica da loja:

`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`.

O nome vai entre aspas. As opções são texto, números ou true/false.

## Peças interativas

Atributos (a loja faz o trabalho, sem JavaScript no tema):

| Atributo | O que faz |
|---|---|
| `data-hystore-add-to-cart="<id do produto>"` | põe o produto no carrinho (pede login antes) |
| `data-hystore-open="cart\|account\|login\|search"` | abre esse lugar |
| `data-hystore-copy="texto"` | copia o texto (IP do servidor...) |
| `data-hystore-color-mode="toggle\|dark\|light"` | troca o modo de cor |
| `data-hystore-language="pt\|en\|es"` | troca a língua |
| `data-hystore-cart-count` | o elemento sempre mostra a quantidade do carrinho |
| `data-hystore-tabs` | botões `[data-hystore-tab="x"]` mostram `[data-hystore-panel="x"]` |
| `data-hystore-accordion` (`="single"` deixa um aberto) | cada `[data-hystore-accordion-item]` tem um `[data-hystore-accordion-toggle]` e um `[data-hystore-accordion-panel]` |
| `data-hystore-modal="id"` | abre `#id`, que tem `data-hystore-modal-panel`; `data-hystore-modal-close` fecha |
| `data-hystore-carousel` | `[data-hystore-slides]` rola, `[data-hystore-prev]`/`[data-hystore-next]`, `data-hystore-autoplay="5000"` |
| `data-hystore-countdown="2026-12-24T23:59:00Z"` | preenche `[data-hystore-days]`, `[data-hystore-hours]`, `[data-hystore-minutes]`, `[data-hystore-seconds]` (ou o próprio texto) |
| `data-hystore-animate` | ganha `data-visible` quando aparece na tela (anime no CSS) |

Elementos que a loja preenche: `<hystore-cart-count>`, `<hystore-account>`
(entrar ou o menu da conta; com `data-cart` mostra também o botão do
carrinho da loja), `<hystore-server-status>`,
`<hystore-online-players>`, `<hystore-search data-placeholder="...">`,
`<hystore-skin data-nick="Steve" data-size="128">`,
`<hystore-video data-youtube="<id ou link>">`,
`<hystore-app data-app="<nome>">` (um app isolado, veja Apps).

Links dentro da loja (`<a href="/loja">`) trocam de página sem recarregar;
páginas de conta e pagamento sempre abrem como página nova.

## Imagens e fontes

Ponha em `assets/` (a ferramenta envia; o servidor confere os 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

Um app é uma pequena página com JavaScript (calculadora, roleta de prêmios,
mini jogo, animação) que o tema coloca com
`<hystore-app data-app="roleta"></hystore-app>`. Ele fica em `apps/<app>/` e
começa pelo `index.html`:

```
apps/roleta/index.html   a página (obrigatória)
apps/roleta/app.js       o script
apps/roleta/app.css      o estilo
apps/roleta/app.json     { "name": "Roleta de prêmios", "height": 420, "permissions": ["customer"] }
```

Ele roda isolado, num quadro em outro domínio (`hystoreapps.cloud`): sem rede
(`fetch`, XHR e WebSocket bloqueados), sem cookies nem armazenamento, sem
formulários, sem janelas novas, e não consegue mudar nem navegar a página da
loja. Ele fala com a loja só pela ponte:

```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();                    // categorias ativas
hystore.addToCart(productId, 1);         // 'added', ou 'login' (a loja pede para entrar)
hystore.navigate('/loja');               // uma página da loja
hystore.open('cart');                    // 'cart' | 'login' | 'account' | 'search'
hystore.context();                       // { locale, colorMode, store: { name, currency } }
hystore.customer();                      // { loggedIn, nick, skinUrl }, só com "customer" no app.json
hystore.resize(600);                     // ou hystore.autosize() para seguir a página
```

- Tudo é dado público: o preço vem da loja, e o carrinho recebe só o id do
  produto. Páginas de dinheiro e de conta nunca mostram apps.
- Carregue só os arquivos do próprio app e o `/sdk/v1.js`; scripts e estilos
  de outros sites, quadros e formulários são recusados ao salvar. Imagens podem
  ser imagens `data:` ou as imagens e fontes do tema pelo endereço completo
  (`https://theme-assets.hystore.com.br/...`, como `GET /v1/theme/assets` e a
  ferramenta `list_assets` do MCP informam).
- `app.json` é opcional: `height` de 40 a 2000 pixels (320 se faltar), `name`
  para leitores de tela, `permissions` só `customer`.
- Limites: 10 apps por tema, 20 arquivos e 512 KB por app.
- Mensagens demais são ignoradas; pôr no carrinho e trocar de página têm
  limites próprios, pequenos.

## Antes de publicar

- `npx @hystudios/hystore theme check` sem erros.
- Todo texto que o visitante lê passa por `| t`, em pt, en e es.
- O tema funciona nos modos de cor que declara e no celular.
- Os links de carrinho, conta e login apontam para `/carrinho`, `/perfil` e
  `/entrar` (ou usam `data-hystore-open`).
