Use when creating the MOBILE version of an existing Vitruvio indicator dashboard panel — a
DesktopPanel shell (`<key>-mobile.xml`) that delegates to `<key>-desktop.xml`, plus the mobile
detection/adaptation added to the desktop form: collapsible filter sidebar, stacked cards,
dark/light theme toggle, and developer mobile preview. Triggers: "criar mobile do dashboard",
"adicionar versão mobile ao indicador", "mobile preview do painel indicador",
"CriarMobileIndicador". Called by vitruvio-criar-indicador-dashboard for the mobile part; can
also be invoked directly with the target `<key>-desktop.xml` path. For the desktop part use
vitruvio-criar-dashboard-desktop.
---
# Criar Mobile para Painel Indicador
> Todas as mensagens ao usuário devem ser em Português.
Você está criando a versão mobile de um painel Vitruvio existente. O padrão usado neste repositório é o **DesktopPanel delegado**: o `<key>-mobile.xml` é um shell mínimo que delega toda a renderização ao `<key>-desktop.xml`, que detecta o contexto mobile e adapta o layout sozinho.
Receba o caminho do `<key>-desktop.xml` alvo como argumento (ex: `panels/meu-painel/meu-painel-desktop.xml`).
---
## LEITURA OBRIGATÓRIA AO INICIAR — CONTEXT.md
**Esta é a primeira coisa a fazer, antes de abrir qualquer outro arquivo.**
Derive o diretório do painel a partir do argumento e leia:
```
panels/<panel-key>/CONTEXT.md
```
**Se o CONTEXT.md existir:**
- Leia-o completo primeiro. Ele contém os IDs reais dos campos, layouts, funções JS e o estado atual do painel — tudo que você precisa para criar o mobile corretamente sem reler o XML inteiro.
- Use a seção **"IDs de layout e campo"** para preencher `forceLayoutsRender` e `forceFieldsRender` no `<key>-mobile.xml`.
- Use a seção **"Funções JavaScript principais"** para entender o que já existe e evitar duplicar.
- Informe ao usuário: _"Li o contexto do painel. Vou criar o mobile com base nele."_
- Ao final, **atualize o CONTEXT.md**: marque a versão mobile como "concluída", adicione o caminho do `<key>-mobile.xml` e registre a modificação no histórico.
**Se o CONTEXT.md não existir:**
- Informe ao usuário que não há arquivo de contexto para este painel.
- Leia o `<key>-desktop.xml` completo para extrair os IDs necessários.
- Ao final, crie o CONTEXT.md completo (veja o formato na Fase 8 da skill **vitruvio-criar-dashboard-desktop**).
---
## Princípios obrigatórios — leia antes de qualquer coisa
### Nunca invente — pergunte quando tiver dúvida
- Se não conhecer um componente, atributo ou comportamento da plataforma: **pergunte ao usuário** antes de inventar. Uma pergunta custa menos do que um bug em produção.
- Baseie-se sempre no que já existe: leia o painel alvo e outros painéis deste repositório para entender o padrão adotado. O que já funciona aqui é a referência.
- Não suponha assinaturas de serviços, nomes de atributos ou comportamentos de componentes que não estejam evidentes no código existente.
### Se o painel tem filtros acima do conteúdo — mova-os para uma sidebar colapsável
Se o painel alvo tem filtros (combos, campos de texto, datas) posicionados **acima** do conteúdo principal (em linha no topo), **não deixe assim no mobile**. Filtros acima ocupam espaço valioso e poluem a tela.
O padrão deste repositório é a **sidebar de filtros colapsável** — os dois painéis de referência
canônicos estão empacotados junto com a skill desktop, leia-os antes de aplicar o padrão:
**No mobile**: o `filterBar` começa invisível (escondido no bloco `_mobileRender` do `run()`). O mobile tem seu próprio botão de filtros dentro da `filterBar` (`btnPreviewFiltros`) que só aparece no mobile — ele aciona o mesmo toggle, mantendo consistência sem adicionar nova lógica.
**Botão de tema dentro da filterBar (obrigatório no mobile)**: adicione um `ButtonWidget id="btnThemeMob"` no fundo do `filterBar` (após o spacer Label), com `visible="false"`. No bloco `_mobileRender` do `run()`, torne-o visível. Ele deve chamar a mesma função `doToggleTheme` que o `btnTheme` do topBar — defina essa função no ScriptWidget InitScript, registre-a com `engine.setGlobalVariable('doToggleTheme', doToggleTheme)` no `init()`, e tenha ambos os botões chamando-a via `engine.getGlobalVariable('doToggleTheme')`. A função atualiza a caption de AMBOS os botões de tema (`btnTheme` e `btnThemeMob`), o CSS do filterBar e o HTML gerado.
**Os campos de filtro dentro da filterBar são os do painel novo** — não copie os campos do `dashboard-contratos`. Leia o painel alvo, identifique seus filtros originais (cmbAno, cmbMes, txtCliente, etc.) e coloque-os dentro do `filterBar` como `VerticalLayout` com `spacing="true" margin="true"`.
**`forceFieldsRender` no `<key>-mobile.xml`**: inclua todos os campos de filtro da sidebar. Sem isso, `engine.getField('cmbAno')` falha no WebView mobile.
**Campo de pesquisa/busca do desktop**: se o painel desktop tem um campo de texto para pesquisa (ex: filtrar por cliente, número, palavra-chave), ele **obrigatoriamente deve aparecer no mobile também**, dentro da filterBar. Nunca omita a pesquisa no mobile — o usuário mobile precisa dela tanto quanto o desktop. Se a pesquisa estiver inline acima da tabela no desktop, mova-a para dentro da `filterBar` no mobile (ou mantenha nos dois lugares).
---
### Siga o padrão do repositório
Este repositório já tem painéis funcionando. Antes de criar algo novo:
- Leia pelo menos um `<key>-desktop.xml` existente deste repo para entender como estão estruturados os componentes, como são chamados os serviços, como é o estilo visual
- Reutilize classes CSS já definidas (ex: `dash-kpi`, `dash-kpi-sm`, `mov-card`, `mobile-topbar`) em vez de criar novas
- Reutilize padrões de `run()` / `init()` / `renderHtml()` que já existem no painel alvo
### Botões e campos padronizados
- **Botões de ação** (`ButtonWidget`): use `style="DEFAULT"` para ações neutras, `style="GREEN"` para confirmar/salvar, `style="RED"` para cancelar/excluir. Nunca crie botões com HTML customizado quando um `ButtonWidget` padrão serve.
- **Campos de filtro**: `ComboBox` para listas fechadas, `TextField` para texto livre, `DateField` para datas. Sempre com `id` único e `caption` em português.
- **Ícones**: use os já presentes no painel alvo. Se precisar de novo ícone, use os codepoints já usados no repositório (ex: `📱` para mobile, `✕` para fechar) — não invente.
- **Labels de valor monetário**: sempre use a função de formatação já existente no painel (ex: `formatBRL(valor)`) — não crie nova.
### JavaScript — ES5 Rhino somente
Nunca use ES6+. Veja o que é proibido e o que usar no lugar:
```javascript
// PROIBIDO
constx=1;// use: var x = 1;
lety=2;// use: var y = 2;
()=>{};// use: function() {}
`texto ${var}`;// use: 'texto ' + var
const{a}=obj;// use: var a = obj.a;
classFoo{}// use: function Foo() {}
async/await// use: callbacks
import/export// não disponível
for(constxofarr)// use: for (var i = 0; i < arr.length; i++)
```
---
---
## Passo 1 — Confirmar repositório e ler o painel alvo
```bash
ls vitruvio.json 2>/dev/null &&echo"OK"||echo"NOT_A_VITRUVIO_REPO"
```
Se `NOT_A_VITRUVIO_REPO`, pare e peça para o usuário entrar no diretório correto.
Leia o `<key>-desktop.xml` alvo **completo** antes de escrever qualquer linha. Leia também pelo menos um outro painel do repositório para entender os padrões já adotados. Só depois prossiga.
Leia o `<key>-desktop.xml` alvo para entender:
- O `formKey` do painel
- Os layouts declarados (`id` de cada `VerticalLayout`, `HorizontalLayout`, `Panel`, etc.)
- Os campos de filtro (`ComboBox`, `TextField`, etc.)
- A estrutura do `initScript` / `run()` / `init()` do ScriptWidget
- O que é renderizado em HTML (ScriptWidget que chama `renderHtml`)
Leia o `vitruvio.json` para identificar a entrada do painel (`key`, `forms`).
---
## Passo 2 — Criar o `<key>-mobile.xml`
Crie `panels/<key>/<key>-mobile.xml`. Ele é um shell que delega ao desktop:
// _mobileRender é setado automaticamente pelo DesktopPanel antes
// do painel desktop inicializar — o desktop detecta e adapta o layout.
}
]]>
</initScript>
<components>
<VerticalLayoutwidth="100%"height="100%">
<DesktopPanelid="<camelCaseId>"panelKey="<key>"
layoutId="rootLayout"external="true"
forceLayoutsRender="<lista de layout ids separados por vírgula>"
forceFieldsRender="<lista de field ids separados por vírgula>"height="100%"/>
</VerticalLayout>
</components>
</form>
</panel-form>
```
**`forceLayoutsRender`**: liste todos os layouts do desktop que precisam existir no DOM móvel (ex: `topBar, filterBar, mainArea, contentPanel, panelRoot`). Leia o XML do desktop para identificá-los.
**`forceFieldsRender`**: liste os campos de filtro que o script usa (ex: `cmbAno, cmbMes, cmbOrdem`). Sem eles, `engine.getField('cmbAno')` falha no mobile.
**`height="100%"` no `VerticalLayout` e no `DesktopPanel`**: os dois níveis do shell usam `100%` — a altura real vem do container mobile que envolve este `<key>-mobile.xml`. Um valor fixo grande (ex: `3000px`/`700px`) já foi usado aqui para forçar os layouts filhos a calcularem porcentagem, mas isso quebrava a tela (espaço em branco/corte); com `100%` nos dois níveis do shell a altura se propaga corretamente do container pai, sem precisar desse artifício.
---
## Passo 3 — Atualizar vitruvio.json
No `vitruvio.json`, na entrada do painel:
- Adicione `"mobile": "panels/<key>/<key>-mobile.xml"` dentro de `"forms"`
- Defina `"showInMobileList": true`
```json
{
"key":"<key>",
"forms":{
"desktop":"panels/<key>/<key>-desktop.xml",
"mobile":"panels/<key>/<key>-mobile.xml"
},
"showInMobileList":true
}
```
---
## Passo 4 — Adaptar o `<key>-desktop.xml`
Esta é a parte principal. O desktop precisa detectar o contexto mobile e adaptar o HTML gerado.
### 4.1 — Bloco de inicialização mobile no `run()`
Dentro do `run()` (initScript), após o bloco de CSS principal, adicione a detecção mobile:
No `addCss()` (ou no CSS principal adicionado em `run()`), inclua os seletores mobile:
> **CRÍTICO:** Todo CSS de scroll mobile **deve estar em `addCss()`** — nunca apenas no bloco `_mobileRender`. Se ficar só em `_mobileRender`, o preview mobile do desenvolvedor não funciona (porque `isMobile=true` mas `_mobileRender` não está setado).
```javascript
// ── Scroll containers no mobile — OBRIGATÓRIO em addCss() ──
// CERTO — loose equality funciona com Java Boolean
if(_wasVisible==true){...}
// OU
if(!!_wasVisible){...}
```
### setExpandRatio antes de addComponent
```javascript
// ERRADO — comp não é filho de base ainda, Vaadin lança IllegalArgumentException
comp.setSizeFull();
base.setExpandRatio(comp,1.0);// ← ERRO: comp não está em base
base.addComponent(comp);
// CERTO — setExpandRatio sempre APÓS addComponent
comp.setSizeFull();
base.removeAllComponents();
base.addComponent(comp);
base.setExpandRatio(comp,1.0);// ← OK: comp já é filho
```
### base.setHeight(null) no preview — por que funciona
`enterMobilePreview` põe `base` (o container do `ScriptWidget`) em altura natural com `base.setHeight(null)` — sem isso, o container mantém a altura da viewport inteira e o `Panel` (`contentPanel`) não enxerga o conteúdo real para rolar. No `_mobileRender` real esse mesmo `base.setHeight(null)`**não** é usado — lá o `renderHtml` sempre chama `comp.setSizeFull()`, porque quem controla a altura do frame é o cálculo de `window.screen.height` no `rootLayout` (seção 4.1), não o `base`.
### inline style sobrescreve class CSS
Se o HTML gerado tem `style="font-size:12px"` no elemento, CSS de classe não sobrescreve (exceto com `!important`). Mude o valor diretamente na geração do HTML para contexto mobile:
### Redimensionar o frame mobile a cada render — não faça isso
Uma versão antiga recalculava a altura do `.mobile-root-frame` para `el.getBoundingClientRect().bottom` a cada `renderHtml`. Isso fazia o frame crescer para caber TODO o conteúdo (não só o viewport), transformando `topBar` + conteúdo num único bloco comprido que a página externa rolava inteiro — "arrastando" a `topBar` junto. A altura do `.mobile-root-frame` é definida **uma única vez** no bloco `_mobileRender` do `run()` (via `window.screen.height`, seção 4.1) e deve permanecer fixa; `renderHtml` só libera `overflow:visible` nos pais intermediários (seção 4.6), nunca redimensiona o frame em si.
---
## Passo 7 — Checklist de entrega
Antes de declarar pronto, confirme:
- [ ] Leu o `<key>-desktop.xml` alvo e ao menos um outro painel do repositório antes de escrever qualquer coisa
- [ ] Tudo que foi feito tem referência no código existente do repositório — nada inventado
- [ ] Não usou nenhuma feature ES6+ (sem `const`, `let`, arrow functions, template literals, destructuring, `class`, `async/await`)
- [ ] Botões usam `style` padrão (`DEFAULT`, `GREEN`, `RED`) — sem HTML customizado para botões
- [ ] Reutilizou funções de formatação já existentes no painel (ex: `formatBRL`) — sem duplicar
- [ ] Reutilizou classes CSS já existentes — sem criar novas desnecessariamente
- [ ]`<key>-mobile.xml` criado com `DesktopPanel` apontando para o `panelKey` correto
- [ ]`forceLayoutsRender` lista todos os layouts usados pelo desktop
- [ ]`forceFieldsRender` lista todos os campos de filtro acessados via `engine.getField()`
- [ ]`vitruvio.json`: `"mobile"` adicionado em `"forms"` e `"showInMobileList": true`
- [ ]`run()` do desktop detecta `_mobileRender` e adapta rootLayout + filterBar + CSS
- [ ] No `_mobileRender` real: `rootLayout` recebe `setHeight("700px")` + `addStyleName('mobile-root-frame')`, seguido de `page.getJavaScript().execute(...)` calculando a altura final a partir de `window.screen.height` (clamp entre 320px e `min(screen.height-220, screen.height*0.80)`) — o `700px` é só o placeholder inicial, não a altura final do mobile real
- [ ] HTML gerado usa `mobileClass` e layouts verticais no mobile
- [ ] Tabelas/grids do desktop viram cards empilhados no mobile
- [ ] Fontes mobile respeitam os tamanhos mínimos (labels ≥ 12px, valores ≥ 16px)
- [ ]`renderHtml` distingue preview (`isMobile` sem `_mobileRender`) de mobile real/desktop: só o preview usa `base.setHeight(null)` + `comp.setWidth("100%")`; os outros dois usam `comp.setSizeFull()` + `base.setExpandRatio(comp, 1.0)`
- [ ]`enterMobilePreview` usa `base.setHeight(null)` (altura natural) — não copia o `700px` fixo do `_mobileRender` real, que só faz sentido para tela de celular
* Descrição: Biblioteca compartilhada dos dashboards de indicadores da Diretoria: preferências de usuário (tema), CSS do topbar e toolbar, helpers de tema.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.