# Native Desktop App

> Transforma um web app local (um servidor que serve uma UI web) num app de desktop Electron sem cara de navegador, com janela sem moldura e a barra de título integrada no cabeçalho do próprio app, no estilo do Obsidian. Use sempre que o usuário falar em "empacotar meu web app", "virar app de desktop", "tirar a barra do Chrome/Edge", "janela frameless", "sem cara de navegador", "controles de janela custom", "igual ao Obsidian", ou quiser que uma ferramenta local pareça um programa nativo em vez de uma aba. Electron no Windows.

- Skill: `gabrielsilvestri/native-desktop-app` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add gabrielsilvestri/native-desktop-app`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gabrielsilvestri/native-desktop-app/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: gabrielsilvestri (https://skillmd.com/u/gabrielsilvestri)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/gabrielsilvestri/native-desktop-app

---


# native-desktop-app

Pega um web app local (servidor Node ou estático servindo HTML/CSS/JS) e entrega um app de desktop Electron que não parece navegador: janela sem moldura, o cabeçalho que o app já tem vira a barra de título, com os botões de minimizar/maximizar/fechar encaixados na ponta direita, e a rolagem começa abaixo dela.

Tudo específico de Electron fica gated em `html.is-electron` (classe posta pelo preload), então o mesmo app continua funcionando igual no navegador comum.

## Os quatro truques que fazem parecer nativo

1. **Electron, não `chrome --app`.** O `--app` sempre desenha a strip de título do navegador, e o Window Controls Overlay só é confiável num PWA instalado, que não dá pra instalar por script. `frame: false` no Electron dá controle total.
2. **A barra de título é o cabeçalho que o app já tem.** Arrastável com `-webkit-app-region: drag`, com os controles custom (SVG, via IPC) na ponta. Não criar uma segunda barra: fica uma faixa vazia em cima do cabeçalho real, e é exatamente o que denuncia o app.
3. **O conteúdo rola abaixo da barra.** Se o body inteiro rola, a barra de rolagem sobe até o topo e bate nos botões da janela. Então o body não rola (`overflow: hidden`) e quem rola é um container que começa na altura da barra.
4. **O servidor roda dentro do Electron.** O processo main sobe o server numa porta livre com o Node embutido (`ELECTRON_RUN_AS_NODE`), então o app não depende de Node instalado na máquina. Fechar a janela mata o servidor.

## Passo a passo

1. Garanta um `package.json` com `name` e `version` (o Electron exige `version`; rode `npm init -y` se não houver). Depois `npm install electron --save-dev`. Se o binário não baixar porque o postinstall foi pulado, rode `node node_modules/electron/install.js`.
2. Copie `templates/main.cjs` e `templates/preload.cjs` pra uma pasta `electron/` na raiz do app, e `templates/app-window.css` pra mesma pasta do `index.html` (onde ficam os estáticos servidos).
3. Adapte os pontos marcados nos templates (tabela abaixo).
4. Linke o CSS no HTML: `<link rel="stylesheet" href="app-window.css">`.
5. No `package.json`, adicione `"main": "electron/main.cjs"` e o script `"app": "electron ."`.
6. Rode `npm run app` e confira as quatro coisas: a barra integrada sem faixa vazia, o arrastar pela barra, os três botões respondendo, o scroll começando abaixo da barra, e que fechar a janela encerra o servidor.

## O que adaptar por app

| Variável | Onde | Como achar |
|---|---|---|
| Entrada do servidor | `main.cjs` → `SERVER_ENTRY` | o arquivo que sobe o server (`server.mjs`, `serve.js`). Precisa ler a porta de `process.env.PORT`; se não lê, ajuste-o, senão o Electron não controla a porta. |
| Título, cor de fundo, ícone | `main.cjs` → `APP_TITLE`, `BG`, `ICON` | o `ICON` é opcional: se o `.ico` não existir, o template ignora. |
| Seletor do cabeçalho | `app-window.css` → `.app-titlebar` | a barra de topo que o app já tem. Se não existe nenhuma, crie um `<header class="app-titlebar">` no topo. |
| Container que rola | `app-window.css` → `#app-scroll` | o wrapper do conteúdo. Troque o seletor ou adicione `id="app-scroll"` nesse elemento. |
| Altura da barra | `app-window.css` → os dois `40px` | o `min-height` da barra e o `top` do container têm que bater. Se o cabeçalho é mais alto, aumente os dois juntos, senão o conteúdo fica escondido embaixo da barra. |

## Erros comuns

- **Barra de rolagem batendo nos controles da janela:** o body está rolando. Mova a rolagem pro container com `top: <altura da barra>` e ponha `overflow: hidden` no body.
- **Duas barras, uma vazia por cima do cabeçalho:** foi criada uma barra de título separada. Use o cabeçalho existente.
- **O visual da janela vazando pro navegador:** algum seletor ficou sem o gate `html.is-electron`.
- **O app exige Node instalado:** o server foi lançado sem `ELECTRON_RUN_AS_NODE`.
- **Link externo abrindo dentro da janela:** falta o `setWindowOpenHandler` com `shell.openExternal`.
- **Os botões da janela não respondem:** com `contextIsolation: true`, o clique tem que chamar `ipcRenderer.send` dentro do próprio preload. Passar por `window.algumaCoisa` não funciona, porque o preload vive em outro mundo de JS.

## Limites

Rodando por `electron .`, o app depende da pasta `node_modules`. Pra um `.exe` distribuível que roda sozinho, o caminho é `electron-builder` (mais uns 150 MB e um passo de build), fora do escopo desta skill.

