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
- 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.
- 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.
- 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.
- 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
- 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.
- 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).
- Adapte os pontos marcados nos templates (tabela abaixo).
- Linke o CSS no HTML:
<link rel="stylesheet" href="app-window.css">.
- No
package.json, adicione "main": "electron/main.cjs" e o script "app": "electron .".
- 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.
1---2name: native-desktop-app3description: 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.4---56# native-desktop-app78Pega 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.910Tudo 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.1112## Os quatro truques que fazem parecer nativo13141. **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.152. **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.163. **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.174. **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.1819## Passo a passo20211. 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`.222. 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).233. Adapte os pontos marcados nos templates (tabela abaixo).244. Linke o CSS no HTML: `<link rel="stylesheet" href="app-window.css">`.255. No `package.json`, adicione `"main": "electron/main.cjs"` e o script `"app": "electron ."`.266. 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.2728## O que adaptar por app2930| Variável | Onde | Como achar |31|---|---|---|32| 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. |33| Título, cor de fundo, ícone | `main.cjs` → `APP_TITLE`, `BG`, `ICON` | o `ICON` é opcional: se o `.ico` não existir, o template ignora. |34| 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. |35| Container que rola | `app-window.css` → `#app-scroll` | o wrapper do conteúdo. Troque o seletor ou adicione `id="app-scroll"` nesse elemento. |36| 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. |3738## Erros comuns3940- **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.41- **Duas barras, uma vazia por cima do cabeçalho:** foi criada uma barra de título separada. Use o cabeçalho existente.42- **O visual da janela vazando pro navegador:** algum seletor ficou sem o gate `html.is-electron`.43- **O app exige Node instalado:** o server foi lançado sem `ELECTRON_RUN_AS_NODE`.44- **Link externo abrindo dentro da janela:** falta o `setWindowOpenHandler` com `shell.openExternal`.45- **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.4647## Limites4849Rodando 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.