create-nodebuilderjs 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +12 -1
- package/package.json +12 -2
- package/template/.agents/skills/nodebuilder-desempenho/SKILL.md +36 -0
- package/template/.agents/skills/nodebuilder-desenho/SKILL.md +54 -0
- package/template/.agents/skills/nodebuilder-empacotar/SKILL.md +37 -0
- package/template/.agents/skills/nodebuilder-graficos/SKILL.md +87 -0
- package/template/.agents/skills/nodebuilder-icones/SKILL.md +53 -0
- package/template/.agents/skills/nodebuilder-imagens/SKILL.md +50 -0
- package/template/.agents/skills/nodebuilder-sem-react/SKILL.md +48 -0
- package/template/.agents/skills/nodebuilder-testes/SKILL.md +56 -0
- package/template/.agents/skills/nodebuilder-ui/SKILL.md +72 -0
- package/template/AGENTS.md +16 -11
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// bun create nodebuilderjs [dir] [--title "Meu App"] [--no-install] [--no-git]
|
|
3
3
|
// Plain JS (not TS) so it runs under both `bun create` and `npm create`.
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, symlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline/promises";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -46,6 +46,17 @@ cpSync(join(here, "template"), target, { recursive: true });
|
|
|
46
46
|
// npm drops .gitignore from published packages, so the template ships it as _gitignore.
|
|
47
47
|
renameSync(join(target, "_gitignore"), join(target, ".gitignore"));
|
|
48
48
|
|
|
49
|
+
// Skills live in .agents/skills (read by Codex, Cursor, OpenCode, Gemini CLI, Copilot). Claude
|
|
50
|
+
// Code only reads .claude/skills, where each skill may be a symlink. Made here, relative,
|
|
51
|
+
// because npm packs and cpSync don't keep relative symlinks.
|
|
52
|
+
const skills = join(target, ".agents", "skills");
|
|
53
|
+
if (existsSync(skills)) {
|
|
54
|
+
mkdirSync(join(target, ".claude", "skills"), { recursive: true });
|
|
55
|
+
for (const skill of readdirSync(skills)) {
|
|
56
|
+
symlinkSync(join("..", "..", ".agents", "skills", skill), join(target, ".claude", "skills", skill), "dir");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
const fill = (file) => {
|
|
50
61
|
const path = join(target, file);
|
|
51
62
|
writeFileSync(path, readFileSync(path, "utf8").replaceAll("{{title}}", title).replaceAll("{{appId}}", appId));
|
package/package.json
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nodebuilderjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Cria um app nodebuilder: bun create nodebuilderjs meu-app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/felipebrgs1/nodebuilderjs.git",
|
|
10
|
+
"directory": "packages/create-nodebuilderjs"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/felipebrgs1/nodebuilderjs#readme",
|
|
13
|
+
"bugs": "https://github.com/felipebrgs1/nodebuilderjs/issues",
|
|
7
14
|
"bin": {
|
|
8
15
|
"create-nodebuilderjs": "./index.js"
|
|
9
16
|
},
|
|
10
|
-
"files": [
|
|
17
|
+
"files": [
|
|
18
|
+
"index.js",
|
|
19
|
+
"template"
|
|
20
|
+
],
|
|
11
21
|
"engines": {
|
|
12
22
|
"node": ">=20"
|
|
13
23
|
},
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-desempenho
|
|
3
|
+
description: Medir e melhorar o desempenho de apps nodebuilder (fps, CPU, memória) e evitar padrões lentos em listas grandes, atualizações frequentes e animações. Use quando o app estiver lento, travando, usando muita CPU/memória, ou ao criar listas com centenas de itens, dados ao vivo ou animações.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Desempenho
|
|
7
|
+
|
|
8
|
+
## Medir
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
NB_STATS=1 bun src/main.tsx # por segundo: fps, ms de estado/layout/pintura/nativo, nós, memória
|
|
12
|
+
NB_MAX_FPS=60 bun src/main.tsx # teto de fps (padrão 120); também run(app, { maxFps: 60 })
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Parado, o app não desenha nada (0% de CPU). Mudanças de estado agendam um frame; várias
|
|
16
|
+
mudanças antes dele saem juntas no mesmo frame.
|
|
17
|
+
|
|
18
|
+
## Padrões
|
|
19
|
+
|
|
20
|
+
- **Listas grandes:** sempre dentro de `<Scroll>`; o que está fora da área visível não é
|
|
21
|
+
posicionado nem desenhado. Mesmo assim cada item vira nós: com milhares, filtre ou pagine.
|
|
22
|
+
- **`<For>` é keyed por referência:** recriar os objetos (`lista.map((x) => ({ ...x }))`)
|
|
23
|
+
destrói e recria todas as linhas. Mantenha as referências e mude só o que mudou; para
|
|
24
|
+
campos que mudam muito, guarde um signal dentro do item (`{ id, cpu: createSignal(0) }`).
|
|
25
|
+
- **Atualize o mínimo:** um signal por valor que muda, e funções finas nos props
|
|
26
|
+
(`text={() => fmt(item.cpu())}`) em vez de um signal gigante com o estado todo.
|
|
27
|
+
- **Várias mudanças juntas:** `batch(() => { setA(1); setB(2); })` gera um único frame e
|
|
28
|
+
roda os effects uma vez.
|
|
29
|
+
- **Animação e dados ao vivo:** use `requestAnimationFrame(cb)` (ritmado pela tela), não
|
|
30
|
+
`setInterval` curto. Para parar, simplesmente não peça o próximo frame.
|
|
31
|
+
- **Texto:** medidas ficam em cache; textos que mudam todo frame (contadores, relógios)
|
|
32
|
+
custam pouco, mas evite gerar milhares de strings novas por frame.
|
|
33
|
+
- **Imagens:** decodificar é caro e síncrono; reutilize o mesmo arquivo (é compartilhado) e
|
|
34
|
+
use arquivos do tamanho exibido.
|
|
35
|
+
- Trabalho pesado de CPU (parse de arquivos grandes, cálculos) trava a interface: divida em
|
|
36
|
+
pedaços com `setTimeout(..., 0)` ou use um `Worker`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-desenho
|
|
3
|
+
description: Desenho livre no nodebuilder com o widget Canvas (linhas, polígonos, arcos, medidores, sparklines, anotações, visualizações sob medida). Use para formas que não são retângulos e gráficos que @nodebuilderjs/core/charts não cobre, no lugar do <canvas> do navegador ou D3. Para gráficos de linha, área, barra e pizza comuns, use a skill nodebuilder-graficos.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Desenho com `<Canvas>`
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import { Canvas, createSignal, theme } from "@nodebuilderjs/core";
|
|
10
|
+
|
|
11
|
+
const [dados, setDados] = createSignal([12, 40, 33, 70, 55, 90]);
|
|
12
|
+
|
|
13
|
+
<Canvas height={160} draw={(g) => {
|
|
14
|
+
const v = dados(); // signals lidos aqui redesenham o canvas
|
|
15
|
+
const x = (i: number) => (i / (v.length - 1)) * g.width;
|
|
16
|
+
const y = (n: number) => g.height - (n / 100) * g.height;
|
|
17
|
+
for (let n = 0; n <= 100; n += 25) g.line(0, y(n), g.width, y(n), "#0000001a", 1, "butt"); // grade
|
|
18
|
+
const pts = v.flatMap((n, i) => [x(i), y(n)]);
|
|
19
|
+
g.polygon([0, g.height, ...pts, g.width, g.height], "#3584e433"); // área
|
|
20
|
+
g.polyline(pts, theme.accent.base, 2); // linha
|
|
21
|
+
g.circle(x(v.length - 1), y(v[v.length - 1]!), 4, theme.accent.base);
|
|
22
|
+
g.text("máx 100", 4, 0, theme.text.secondary, 11);
|
|
23
|
+
}} />
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## API (`g`)
|
|
27
|
+
|
|
28
|
+
| Método | O quê |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `g.width`, `g.height` | tamanho do canvas (dentro do `padding`) |
|
|
31
|
+
| `rect(x, y, w, h, cor, raio?)` | retângulo (barras) |
|
|
32
|
+
| `line(x0, y0, x1, y1, cor, largura = 1, ponta = "round")` | segmento; ponta `round`, `butt` (reta) ou `square` |
|
|
33
|
+
| `polyline(pontos, cor, largura = 1, { cap, closed })` | linha por vários pontos `[x0, y0, x1, y1, ...]`, junções redondas |
|
|
34
|
+
| `polygon(pontos, cor)` | polígono preenchido (côncavo pode; cruzando a si mesmo não desenha) |
|
|
35
|
+
| `circle(cx, cy, raio, cor)` | disco |
|
|
36
|
+
| `arc(cx, cy, raioExterno, raioInterno, início, varredura, cor)` | fatia (interno 0) ou anel; radianos, 0 = cima, sentido horário |
|
|
37
|
+
| `text(texto, x, y, cor, tamanho?, negrito?)`, `measureText(texto, tamanho?)` | rótulos; `y` é o topo da linha |
|
|
38
|
+
|
|
39
|
+
## Regras
|
|
40
|
+
|
|
41
|
+
- Dê tamanho ao canvas: `width`/`height` ou `grow`. Sem tamanho, `draw` não roda.
|
|
42
|
+
- Coordenadas locais, com `y` para baixo. Inverta para gráficos: `g.height - valor * escala`.
|
|
43
|
+
- `draw` deve só desenhar: nada de criar signals, effects ou chamar `set` lá dentro.
|
|
44
|
+
- O canvas redesenha quando um signal lido em `draw` muda ou o tamanho muda; a geometria
|
|
45
|
+
fica gravada, então outras partes da janela redesenhando não custam nada aqui.
|
|
46
|
+
- Dados ao vivo: atualize o signal com `requestAnimationFrame`; milhares de pontos
|
|
47
|
+
funcionam (5000 pontos de linha + área a ~120 fps).
|
|
48
|
+
- Pizza/rosca: some as fatias em radianos (`fração * 2 * Math.PI`); um pequeno vão
|
|
49
|
+
(`- 0.03` na varredura) separa as fatias visualmente.
|
|
50
|
+
- Cores translúcidas em `polyline` escurecem nas junções: prefira cores opacas para linhas
|
|
51
|
+
e translúcidas para áreas.
|
|
52
|
+
- Interação (tooltip, clique num ponto, arrastar): use `onPointerMove`/`onClick` no próprio
|
|
53
|
+
`Canvas`; `e.localX`/`e.localY` estão nas mesmas coordenadas do desenho (o `padding`
|
|
54
|
+
desloca: subtraia-o).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-empacotar
|
|
3
|
+
description: Gerar e distribuir o app nodebuilder (AppImage, .deb, .rpm, .tar.gz) com nodebuilder package, incluir arquivos (imagens, dados) no executável e configurar nome, ícone e app id. Use ao preparar uma versão para instalar, ao mudar o nome/ícone do app ou quando o app empacotado se comporta diferente do bun run dev.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Empacotar
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
bun run package # = nodebuilder package src/main.tsx --title "..." --app-id ...
|
|
10
|
+
bunx nodebuilder package src/main.tsx --targets deb,rpm --icon assets/icone.svg --version 1.2.0
|
|
11
|
+
bunx nodebuilder package --help
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Gera em `dist/` um executável único (Bun + app + núcleo nativo, ~31 MB compactado) dentro de
|
|
15
|
+
AppImage, `.deb`, `.rpm` e `.tar.gz`, com atalho no menu e ícone. Roda em distros com glibc
|
|
16
|
+
2.34+ (Ubuntu 22.04+, Debian 12+, Fedora, RHEL 9+).
|
|
17
|
+
|
|
18
|
+
## Regras
|
|
19
|
+
|
|
20
|
+
- **Arquivos do app** (imagens, JSON, sons) entram no executável só se forem importados com
|
|
21
|
+
`with { type: "file" }` (o import vira o caminho). `readFileSync("./dados.json")` com
|
|
22
|
+
caminho solto funciona no dev e **falha no pacote**. Para JSON, `import dados from
|
|
23
|
+
"./dados.json"` também embute.
|
|
24
|
+
- O `appId` da `<Window>` precisa ser igual ao `--app-id` (o template já faz isso); senão o
|
|
25
|
+
ícone e o agrupamento na barra de tarefas não funcionam.
|
|
26
|
+
- `--icon`: `.svg` ou PNG 256x256. `--name` é o nome do executável/pacote (minúsculas).
|
|
27
|
+
- Bibliotecas do sistema (Wayland, X11, EGL, FreeType, HarfBuzz, fontconfig) são
|
|
28
|
+
dependências declaradas dos pacotes, nunca embutidas. `libheif` (só para AVIF/HEIC) é
|
|
29
|
+
apenas sugerida.
|
|
30
|
+
- Dados do usuário: salve em `~/.local/share/<app>` (`process.env.XDG_DATA_HOME`), nunca
|
|
31
|
+
ao lado do executável (em `/usr/bin` não dá para escrever).
|
|
32
|
+
|
|
33
|
+
## Verificar
|
|
34
|
+
|
|
35
|
+
- Rode o executável gerado fora da pasta do projeto (`cd / && caminho/do/app`) para pegar
|
|
36
|
+
arquivos que só existiam no disco do desenvolvimento.
|
|
37
|
+
- AppImage: `chmod +x dist/*.AppImage && ./dist/*.AppImage`.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-graficos
|
|
3
|
+
description: Gráficos de linha, área, barra, composto e pizza/rosca no nodebuilder com @nodebuilderjs/core/charts, que usa a mesma API do Recharts. Use ao criar dashboards, relatórios, séries temporais, comparações por categoria ou participação (pizza), e sempre que pensaria em usar Recharts, Chart.js ou Victory.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gráficos com `@nodebuilderjs/core/charts`
|
|
7
|
+
|
|
8
|
+
Já vem no `@nodebuilderjs/core` (não instale nada); só entra no app se for importado.
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { createSignal, theme } from "@nodebuilderjs/core";
|
|
12
|
+
import {
|
|
13
|
+
Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, Legend, Line, LineChart, Pie, PieChart, XAxis, YAxis,
|
|
14
|
+
} from "@nodebuilderjs/core/charts";
|
|
15
|
+
|
|
16
|
+
const [vendas] = createSignal([
|
|
17
|
+
{ mes: "jan", receita: 120, meta: 100 },
|
|
18
|
+
{ mes: "fev", receita: 150, meta: 110 },
|
|
19
|
+
{ mes: "mar", receita: 90, meta: 120 },
|
|
20
|
+
]);
|
|
21
|
+
const canais = [{ nome: "Loja", valor: 42 }, { nome: "Site", valor: 31 }];
|
|
22
|
+
|
|
23
|
+
export const graficos = () => (
|
|
24
|
+
<>
|
|
25
|
+
<LineChart data={vendas} height={240}>
|
|
26
|
+
<CartesianGrid vertical={false} />
|
|
27
|
+
<XAxis dataKey="mes" />
|
|
28
|
+
<YAxis tickFormatter={(v) => `R$ ${v}`} />
|
|
29
|
+
<Line dataKey="receita" name="Receita" />
|
|
30
|
+
<Line dataKey="meta" name="Meta" stroke="#e5a50a" type="monotone" dot={false} />
|
|
31
|
+
<Legend />
|
|
32
|
+
</LineChart>
|
|
33
|
+
<BarChart data={vendas} height={200}>
|
|
34
|
+
<XAxis dataKey="mes" />
|
|
35
|
+
<YAxis />
|
|
36
|
+
<Bar dataKey="receita" fill={theme.accent.base} radius={3} />
|
|
37
|
+
</BarChart>
|
|
38
|
+
<AreaChart data={vendas} height={160}>
|
|
39
|
+
<YAxis domain={[0, 200]} />
|
|
40
|
+
<Area dataKey="receita" type="monotone" />
|
|
41
|
+
</AreaChart>
|
|
42
|
+
<PieChart height={220}>
|
|
43
|
+
<Pie data={canais} dataKey="valor" nameKey="nome" innerRadius="50%" paddingAngle={2}>
|
|
44
|
+
<Cell fill="#3584e4" />
|
|
45
|
+
<Cell fill="#26a269" />
|
|
46
|
+
</Pie>
|
|
47
|
+
<Legend />
|
|
48
|
+
</PieChart>
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## O que é suportado
|
|
54
|
+
|
|
55
|
+
| Componente | Props |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `LineChart`, `AreaChart`, `BarChart`, `ComposedChart` (mistura) | `data` (array ou signal), `height` (240), `width` ou `grow` e as props de estilo |
|
|
58
|
+
| `Line` | `dataKey`, `name`, `stroke`, `strokeWidth` (2), `strokeDasharray` (`"6 4"`), `dot` (true), `type` (`linear`/`monotone`) |
|
|
59
|
+
| `Area` | `dataKey`, `name`, `stroke`, `fill`, `fillOpacity` (0,25), `type`, `stackId` |
|
|
60
|
+
| `Bar` | `dataKey`, `name`, `fill`, `radius`, `stackId`, `label` (true ou `(valor) => texto`) |
|
|
61
|
+
| `XAxis` | `dataKey`, `tickFormatter`, `hide` |
|
|
62
|
+
| `YAxis` | `domain={[min, max]}`, `tickCount`, `tickFormatter`, `hide` |
|
|
63
|
+
| `CartesianGrid` | `horizontal`, `vertical`, `stroke`, `strokeDasharray` |
|
|
64
|
+
| `PieChart` + `Pie` | `data`, `dataKey`, `nameKey`, `innerRadius`, `outerRadius` (px ou `"80%"`), `paddingAngle` (graus), `label` (true ou `({ name, value, percent }) => texto`); `<Cell fill>` por fatia |
|
|
65
|
+
| `Tooltip` | `formatter(valor, nome)`, `labelFormatter(categoria)`; linha-guia na categoria sob o mouse (na pizza: a fatia) |
|
|
66
|
+
| `Legend` | sem props (embaixo, centralizada) |
|
|
67
|
+
|
|
68
|
+
`dataKey` é o nome do campo ou uma função `(linha) => valor`.
|
|
69
|
+
|
|
70
|
+
## Diferenças do Recharts
|
|
71
|
+
|
|
72
|
+
- **Sem `ResponsiveContainer`:** o gráfico já ocupa a largura do pai; dê `height`.
|
|
73
|
+
- Séries com o mesmo `stackId` se empilham (barras numa coluna só, áreas uma sobre a outra);
|
|
74
|
+
o eixo y considera os totais.
|
|
75
|
+
- O eixo x omite rótulos que se sobreporiam, mantendo o último (como `interval="preserveEnd"`).
|
|
76
|
+
- Não existem ainda: `Brush`, `ReferenceLine`, `ScatterChart`, `RadarChart`, eixos
|
|
77
|
+
secundários, animações; em `Line`, o `label`.
|
|
78
|
+
- Pizza começa no topo e gira no sentido horário (`startAngle`/`endAngle` não existem); os
|
|
79
|
+
rótulos ficam fora das fatias, então deixe `outerRadius` em ~70% para caberem.
|
|
80
|
+
- Cores: sem `stroke`/`fill`, cada série pega a próxima cor da paleta (a primeira é
|
|
81
|
+
`theme.accent.base`).
|
|
82
|
+
- Para algo fora disso, desenhe com `<Canvas>` (skill `nodebuilder-desenho`).
|
|
83
|
+
|
|
84
|
+
## Desempenho
|
|
85
|
+
|
|
86
|
+
Mudar o signal de `data` redesenha só aquele gráfico. Atualizar a cada frame (dados ao
|
|
87
|
+
vivo com `requestAnimationFrame`) funciona; centenas de pontos por série são tranquilos.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-icones
|
|
3
|
+
description: Ícones e SVG no nodebuilder com o widget Icon, usando Lucide (lucide-static ou lucide) e Phosphor (@phosphor-icons/core). Use ao adicionar ícones a botões, menus, barras laterais ou listas, ao escolher uma biblioteca de ícones, ou ao mostrar qualquer arquivo .svg.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ícones
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
bun add lucide-static # arquivos .svg do Lucide
|
|
10
|
+
bun add lucide # ou: os mesmos ícones como dados (import { Camera } from "lucide")
|
|
11
|
+
bun add @phosphor-icons/core # Phosphor: assets/<peso>/<nome>.svg
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { Button, Icon, Row, Text, createSignal, theme } from "@nodebuilderjs/core";
|
|
16
|
+
import camera from "lucide-static/icons/camera.svg" with { type: "text" };
|
|
17
|
+
import heart from "@phosphor-icons/core/assets/duotone/heart-duotone.svg" with { type: "text" };
|
|
18
|
+
import { Star } from "lucide";
|
|
19
|
+
|
|
20
|
+
const [ativo] = createSignal(false);
|
|
21
|
+
|
|
22
|
+
<Icon svg={camera} /> // 20 px, cor do texto ao redor
|
|
23
|
+
<Icon svg={heart} size={32} color="#e01b24" />
|
|
24
|
+
<Icon svg={Star} size={16} strokeWidth={1.5} /> // espessura do traço (ícones de contorno)
|
|
25
|
+
<Button variant="primary"><Icon svg={Star} size={16} /><Text>Favoritar</Text></Button>
|
|
26
|
+
<Row color={() => (ativo() ? theme.accent.base : theme.text.secondary)}>
|
|
27
|
+
<Icon svg={camera} /><Text>Câmera</Text> {/* ícone e texto mudam de cor juntos */}
|
|
28
|
+
</Row>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Regras
|
|
32
|
+
|
|
33
|
+
- **Nunca** use `lucide-react`, `lucide-solid`, `@phosphor-icons/react`, `react-icons` ou
|
|
34
|
+
`@heroicons/react`: eles exigem React/DOM. Use os pacotes acima.
|
|
35
|
+
- Importe arquivos `.svg` com `with { type: "text" }` (o import vira o texto do SVG e o
|
|
36
|
+
`nodebuilder package` o embute). `with { type: "file" }` (caminho) também funciona.
|
|
37
|
+
- `svg` aceita: texto do SVG, caminho de um `.svg` ou os dados do pacote `lucide`. Pode ser
|
|
38
|
+
uma função (`svg={() => (aberto() ? chevronUp : chevronDown)}`).
|
|
39
|
+
- `size` é a altura em px lógicos (padrão 20, `theme.icon.size`); a largura segue o
|
|
40
|
+
`viewBox`. A cor vem de `color` no próprio `Icon` ou é herdada de um pai, como no `Text`.
|
|
41
|
+
- Trocar a cor é de graça (ícones de uma cor são tingidos na GPU); trocar o tamanho
|
|
42
|
+
rasteriza de novo (~0,5 ms). Ficam nítidos em telas HiDPI.
|
|
43
|
+
- Pesos do Phosphor: `thin`, `light`, `regular`, `bold`, `fill`, `duotone` (pastas em
|
|
44
|
+
`assets/`; nomes com o sufixo do peso, exceto `regular`: `heart.svg`, `heart-fill.svg`).
|
|
45
|
+
|
|
46
|
+
## Limites do SVG
|
|
47
|
+
|
|
48
|
+
Suportado: `path` (com arcos e curvas), `rect`, `circle`, `ellipse`, `line`, `polyline`,
|
|
49
|
+
`polygon`, `g`, `transform`, `fill`/`stroke` (cores, `currentColor`), espessura, pontas,
|
|
50
|
+
junções, `fill-rule`, opacidades e `style=""` inline.
|
|
51
|
+
Não suportado (o resto do desenho aparece): gradientes, padrões, texto, filtros, máscaras,
|
|
52
|
+
recortes, `<use>`, tracejado e `<style>`. Logos e ilustrações assim: exporte um PNG 2x e
|
|
53
|
+
use `Image`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-imagens
|
|
3
|
+
description: Mostrar imagens (PNG, JPEG, WebP, AVIF, HEIC) com o widget Image do nodebuilder e incluir os arquivos no app empacotado. Use ao adicionar fotos, logos, avatares, miniaturas ou qualquer arquivo de imagem à interface. Para ícones e SVG, use a skill nodebuilder-icones.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Imagens
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import { Image } from "@nodebuilderjs/core";
|
|
10
|
+
import logo from "./assets/logo.png" with { type: "file" };
|
|
11
|
+
|
|
12
|
+
<Image src={logo} width={64} /> // altura segue a proporção
|
|
13
|
+
<Image src={foto} width={200} height={120} fit="cover" radius={12} />
|
|
14
|
+
<Image src={() => avatarDe(usuario())} width={32} height={32} radius={16} opacity={0.9} />
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Regras
|
|
18
|
+
|
|
19
|
+
- **Importe arquivos com `with { type: "file" }`.** O import vira o caminho do arquivo, e o
|
|
20
|
+
`nodebuilder package` embute a imagem no executável. `new URL("./x.png", import.meta.url)`
|
|
21
|
+
e caminhos soltos funcionam no `bun run dev`, mas **quebram no app empacotado**.
|
|
22
|
+
- `src` aceita: caminho (relativo ao diretório atual), `URL` `file:`, bytes do arquivo
|
|
23
|
+
(`Uint8Array`, ex.: de um `fetch`) ou pixels `{ width, height, data }` (RGBA 8 bits).
|
|
24
|
+
Pode ser uma função: trocar o `src` libera a imagem antiga.
|
|
25
|
+
- Tamanho: sem `width`/`height`, usa os pixels do arquivo (1 pixel = 1 px lógico). Com só um
|
|
26
|
+
dos dois, o outro segue a proporção. Para telas HiDPI, use um arquivo 2x maior e fixe o
|
|
27
|
+
tamanho (`width={64}` para um PNG de 128 px).
|
|
28
|
+
- `fit`: `contain` (padrão, imagem inteira centralizada), `cover` (preenche e corta), `fill`
|
|
29
|
+
(estica). `radius` arredonda; `opacity` vai de 0 a 1.
|
|
30
|
+
- O mesmo arquivo em vários lugares é decodificado uma vez; a memória é liberada quando o
|
|
31
|
+
último `Image` sai da tela.
|
|
32
|
+
- Arquivo que não carrega: aviso no console e nada desenhado (o app não quebra).
|
|
33
|
+
|
|
34
|
+
## Formatos
|
|
35
|
+
|
|
36
|
+
| Formato | Como | Observação |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| PNG, JPEG, WebP | decodificados em TypeScript | sempre funcionam; JPEG respeita a orientação do EXIF |
|
|
39
|
+
| AVIF, HEIC | biblioteca do sistema `libheif` | sem ela, só esses dão erro ("needs libheif"); o pacote `.deb`/`.rpm` a sugere, mas não instala. Prefira WebP/PNG/JPEG para imagens do próprio app |
|
|
40
|
+
| SVG | pelo widget `Icon` (skill `nodebuilder-icones`) | subconjunto para ícones; ilustrações com gradiente/texto: exporte PNG 2x |
|
|
41
|
+
| GIF animado, vídeo | não suportado | |
|
|
42
|
+
|
|
43
|
+
## Cuidados
|
|
44
|
+
|
|
45
|
+
- A decodificação é síncrona: uma foto de 4 megapixels leva ~0,1 s e trava a interface
|
|
46
|
+
nesse meio-tempo. Reduza imagens grandes antes de colocar no app (ex.: miniaturas).
|
|
47
|
+
- Imagens muito reduzidas na tela ficam serrilhadas (sem mipmaps): use arquivos perto do
|
|
48
|
+
tamanho exibido.
|
|
49
|
+
- Em `bun test`, `Image` funciona sem janela (as texturas são simuladas); AVIF/HEIC
|
|
50
|
+
precisam do addon nativo e da libheif.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-sem-react
|
|
3
|
+
description: Tradução de hábitos de React, DOM, CSS e bibliotecas web para o nodebuilder, e quais pacotes npm funcionam. Use antes de instalar qualquer biblioteca de UI, ícones, componentes, estado ou estilo (lucide-react, MUI, Tailwind, styled-components, react-*, solid-js...), ao escrever useState/useEffect/className/div/onChange, ou ao portar código React/Solid/HTML para este app.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Vindo do React (ou da web)
|
|
7
|
+
|
|
8
|
+
O nodebuilder desenha a interface com a GPU, sem navegador: **não existe DOM, CSS, React
|
|
9
|
+
nem Solid**. O JSX parece o de sempre, mas os elementos são os widgets do
|
|
10
|
+
`@nodebuilderjs/core`.
|
|
11
|
+
|
|
12
|
+
## Tradução direta
|
|
13
|
+
|
|
14
|
+
| Na web / React | No nodebuilder |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `useState(0)` | `const [n, setN] = createSignal(0)`; leia com `n()` |
|
|
17
|
+
| `useMemo` | `createMemo(() => ...)` |
|
|
18
|
+
| `useEffect(() => ..., [deps])` | `createEffect(() => ...)` (deps automáticas) + `onCleanup` |
|
|
19
|
+
| re-render do componente | não existe: o componente roda **uma vez** |
|
|
20
|
+
| `{count}` em JSX | `{count}` (a função, não `count()`), senão fica fixo |
|
|
21
|
+
| `style={{ padding: 8 }}`, `className` | props: `padding={8}`, `background="#fff"` |
|
|
22
|
+
| `div`, `span`, `p`, `h1` | `Column`, `Row`, `Box`, `Text fontSize={24} bold` |
|
|
23
|
+
| `onChange` de input | `TextField value={v} onInput={setV}` |
|
|
24
|
+
| `list.map(...)` em JSX | `<For each={list}>{(item) => ...}</For>` |
|
|
25
|
+
| `cond && <X/>` | `<Show when={() => cond()}><X/></Show>` |
|
|
26
|
+
| `useContext` | módulos com signals exportados (estado global simples) |
|
|
27
|
+
| CSS `:hover` | `onPointerEnter`/`onPointerLeave` + signal |
|
|
28
|
+
| `fetch`, `fs`, timers | funcionam (APIs do Bun/Node); atualize signals com o resultado |
|
|
29
|
+
|
|
30
|
+
## Bibliotecas npm
|
|
31
|
+
|
|
32
|
+
- **Funcionam:** lógica pura sem DOM (date-fns, zod, lodash, clientes HTTP, SQLite do Bun,
|
|
33
|
+
parsers). Ícones em SVG puro com o widget `Icon`: `lucide-static`, `lucide` (os dados,
|
|
34
|
+
não o `createIcons()`) e `@phosphor-icons/core` (skill `nodebuilder-icones`).
|
|
35
|
+
- **Não funcionam:** tudo que renderiza React/Solid/Vue ou mexe no DOM: `lucide-react`,
|
|
36
|
+
`lucide-solid`, `@phosphor-icons/react`, MUI, Chakra, shadcn/ui, Radix, Tailwind,
|
|
37
|
+
styled-components, react-router, bibliotecas de gráficos em canvas/SVG do navegador.
|
|
38
|
+
- Gráficos: use `@nodebuilderjs/core/charts` (mesma API do Recharts; skill
|
|
39
|
+
`nodebuilder-graficos`) ou desenhe com `<Canvas>` (skill `nodebuilder-desenho`). Recharts,
|
|
40
|
+
Chart.js e D3 precisam do DOM e não funcionam.
|
|
41
|
+
- Na dúvida: se o pacote importa `react`, `react-dom`, `solid-js/web` ou usa `document`,
|
|
42
|
+
ele não serve aqui.
|
|
43
|
+
|
|
44
|
+
## Erros comuns
|
|
45
|
+
|
|
46
|
+
- Ler o signal no corpo do componente (`const x = count();`) e esperar que atualize.
|
|
47
|
+
- Desestruturar props reativas (`const { value } = props`) e perder a função.
|
|
48
|
+
- Colocar `createSignal` dentro de um effect para "resetar" estado: crie fora e use `set`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-testes
|
|
3
|
+
description: Escrever e depurar testes de apps nodebuilder com bun test, sem abrir janela (mount, findByText, click, interact para mouse e teclado, layout, debugTree). Use ao criar testes, ao verificar uma mudança de comportamento ou layout, ou quando um teste de componente falha.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testes sem janela
|
|
7
|
+
|
|
8
|
+
`bun test` roda os arquivos de `test/`. Nada abre janela: `mount` monta a árvore com
|
|
9
|
+
métricas de texto falsas (**8 px por caractere, linha de 20 px**), então o layout é
|
|
10
|
+
determinístico.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { expect, test } from "bun:test";
|
|
14
|
+
import { click, findAll, findByText, mount } from "@nodebuilderjs/core/testing";
|
|
15
|
+
import { debugTree } from "@nodebuilderjs/core";
|
|
16
|
+
import { App } from "../src/App.js";
|
|
17
|
+
|
|
18
|
+
test("adiciona uma tarefa", () => {
|
|
19
|
+
const { root, relayout, dispose } = mount(() => <App />, { width: 800, height: 600 });
|
|
20
|
+
click(findByText(root, "Adicionar")!); // sobe até o pai com onClick (o Button)
|
|
21
|
+
expect(findByText(root, /1 tarefa/)).toBeDefined();
|
|
22
|
+
relayout(); // recalcula posições após mudanças
|
|
23
|
+
const item = findByText(root, "Nova tarefa")!;
|
|
24
|
+
expect(item.y).toBeGreaterThan(0);
|
|
25
|
+
dispose(); // descarta effects e signals do app
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API (`@nodebuilderjs/core/testing`)
|
|
30
|
+
|
|
31
|
+
- `mount(view, size?)` → `{ root, relayout, dispose }`. Layout feito uma vez no `mount`;
|
|
32
|
+
chame `relayout()` antes de checar `x`, `y`, `width`, `height` depois de mudar estado.
|
|
33
|
+
- `findByText(root, "texto" | /regex/)`: primeiro `Text` cujo conteúdo inteiro bate.
|
|
34
|
+
- `findAll(root, (n) => ...)`: busca livre; `n.label` é `"Text \"...\""`, `"Button"`, `"Row"`...
|
|
35
|
+
- `click(node)`: dispara o `onClick` do nó ou do ancestral mais próximo (atalho rápido).
|
|
36
|
+
- `interact(root)`: mouse e teclado entregues como no app de verdade (hover, foco, captura
|
|
37
|
+
ao arrastar, Tab): `hover(n)`, `press(n)`, `release(n)`, `drag(n, dx, dy)`, `move(x, y)`,
|
|
38
|
+
`scroll(n, dy)`, `key("Return")`, `type("texto")` e `focused`.
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
const ui = interact(root);
|
|
42
|
+
ui.press(campo); // foca o TextField
|
|
43
|
+
ui.type("Ana"); // digita no campo focado
|
|
44
|
+
ui.key("Return"); // dispara o onSubmit
|
|
45
|
+
ui.drag(slider, 80, 0); // arrasta 80 px para a direita
|
|
46
|
+
```
|
|
47
|
+
- `debugTree(node)` (de `@nodebuilderjs/core`): árvore com posições, ótimo para ver o layout.
|
|
48
|
+
|
|
49
|
+
## Dicas
|
|
50
|
+
|
|
51
|
+
- Estado muda na hora (signals são síncronos); não há `await` nem `act()`.
|
|
52
|
+
- Prefira `interact` a chamar handlers à mão: ele testa também foco, hover e propagação.
|
|
53
|
+
Depois de mudanças que movem nós, chame `relayout()` antes de interagir de novo.
|
|
54
|
+
- Teste cálculos de layout com tamanhos fixos (`width`, `height`) para não depender das
|
|
55
|
+
métricas falsas.
|
|
56
|
+
- Antes de concluir uma tarefa: `bun run typecheck && bun test`.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-ui
|
|
3
|
+
description: Widgets, props de estilo e layout do nodebuilder (Window, Column, Row, Scroll, Text, Button, TextField, Checkbox, Card, For, Show, theme). Use ao criar ou mudar qualquer tela, formulário, lista, barra lateral ou layout de um app nodebuilder, ou quando um elemento não aparece, não cresce ou não rola como esperado.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Interface com nodebuilder
|
|
7
|
+
|
|
8
|
+
Tudo vem de `@nodebuilderjs/core`. Não há DOM nem CSS: widgets são funções e o estilo são
|
|
9
|
+
props planas. Reatividade estilo SolidJS: **só funções são reativas** (veja o AGENTS.md).
|
|
10
|
+
|
|
11
|
+
## Widgets
|
|
12
|
+
|
|
13
|
+
| Widget | Para quê | Props principais |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `Window` | raiz, uma por app, com barra de título própria | `title`, `width`, `height` (tamanho inicial), `appId`, `onClose` (retorne `false` para não fechar) |
|
|
16
|
+
| `Column` / `Row` / `Box` | contêiner flex (coluna, linha; `Box` é coluna) | estilo + eventos |
|
|
17
|
+
| `Scroll` | coluna com rolagem vertical | precisa de `grow={1}` ou `height`; filhos podem ser maiores |
|
|
18
|
+
| `Text` | texto de uma linha (sem quebra automática) | filhos (`{count}`, `"a"`) ou `text={() => ...}` |
|
|
19
|
+
| `Image` | PNG, JPEG, WebP, AVIF | veja a skill `nodebuilder-imagens` |
|
|
20
|
+
| `Icon` | ícone SVG (Lucide, Phosphor), cor herdada como texto | `svg`, `size` (20), `color`, `strokeWidth`; skill `nodebuilder-icones` |
|
|
21
|
+
| `Canvas` | desenho livre: linhas, polígonos, arcos, texto | `draw={(g) => ...}` + tamanho; skill `nodebuilder-desenho` |
|
|
22
|
+
| `Button` | botão com foco e teclado | `label` ou filhos, `variant` (`default`, `primary`, `danger`, `flat`), `disabled`, `onClick` |
|
|
23
|
+
| `TextField` | campo de texto controlado | `value` (accessor), `onInput(valor)`, `onSubmit`, `placeholder` |
|
|
24
|
+
| `Checkbox` | caixa de seleção | `checked` (accessor), `onChange(marcado)`, `label` |
|
|
25
|
+
| `Card` | coluna com fundo, borda, raio 12, padding 16, gap 8 | estilo |
|
|
26
|
+
| `Divider` / `Spacer` | linha de 1 px / espaço flexível (`size` fixo opcional) | |
|
|
27
|
+
| `<For each={lista}>{(item, i) => ...}</For>` | lista keyed por referência; `i` é um accessor | `fallback` |
|
|
28
|
+
| `<Show when={() => ...} fallback={...}>` | condicional | |
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
const [nome, setNome] = createSignal("");
|
|
32
|
+
<TextField value={nome} onInput={setNome} onSubmit={salvar} placeholder="Nome" />
|
|
33
|
+
<Checkbox checked={ativo} onChange={setAtivo} label="Ativo" />
|
|
34
|
+
<Button label="Salvar" variant="primary" disabled={() => !nome()} onClick={salvar} />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Props de estilo (qualquer widget)
|
|
38
|
+
|
|
39
|
+
- Layout: `direction` (`"row"`/`"column"`), `gap`, `padding` (`8`, `[v, h]` ou
|
|
40
|
+
`[top, right, bottom, left]`), `width`, `height`, `minWidth`, `minHeight`, `maxWidth`,
|
|
41
|
+
`maxHeight`, `grow`, `align`, `justify`, `position: "absolute"` + `left`/`top`, `clip`.
|
|
42
|
+
- Visual: `background`, `radius`, `borderWidth`, `borderColor`, `shadow={{ color, blur, y }}`.
|
|
43
|
+
- Texto (herdados pelos filhos): `color`, `fontSize`, `bold`. Cursor: `cursor="pointer"`.
|
|
44
|
+
- Cores são strings hex (`"#3584e4"`, `"#00000010"` com alfa) ou `"transparent"`.
|
|
45
|
+
- Eventos em qualquer contêiner: `onClick`, `onPointerDown/Up/Enter/Leave`,
|
|
46
|
+
`onPointerMove`, `onScroll`, `onKeyDown`, `onFocus`, `onBlur`. Eventos de ponteiro trazem
|
|
47
|
+
`x`/`y` (janela) e `localX`/`localY` (relativos ao nó); enquanto o botão está pressionado,
|
|
48
|
+
`onPointerMove` continua chegando ao nó onde o clique começou, mesmo fora dele (arrastar,
|
|
49
|
+
sliders). `onKeyDown` só chega pelo caminho do elemento focado.
|
|
50
|
+
- Qualquer prop aceita valor fixo ou função: `background={() => (sel() ? a : b)}`.
|
|
51
|
+
|
|
52
|
+
## Regras do layout (flexbox simplificado)
|
|
53
|
+
|
|
54
|
+
- `align` padrão é **stretch**: filhos ocupam toda a largura de uma coluna. Use
|
|
55
|
+
`align="start"`/`"center"` para encolher ao conteúdo.
|
|
56
|
+
- `justify`: `start`, `center`, `end`, `space-between`. Não existe `space-around`/`evenly`.
|
|
57
|
+
- `grow={1}` divide o espaço que sobra. Sem `grow`, o tamanho é o do conteúdo.
|
|
58
|
+
- Um filho **não encolhe abaixo do próprio conteúdo** (como `min-size: auto` do CSS), exceto
|
|
59
|
+
`Scroll`, `clip` ou `minWidth/minHeight` explícito. Lista longa dentro de uma coluna
|
|
60
|
+
que cresce sem parar? Envolva em `<Scroll grow={1}>`.
|
|
61
|
+
- Para um `Scroll` dentro de uma coluna com `grow`, a coluna também precisa de `minHeight={0}`.
|
|
62
|
+
- Não há porcentagens, `flex-wrap`, grid nem quebra de linha de texto: monte linhas com `Row`
|
|
63
|
+
e larguras fixas (veja tabelas: `Text width={110}`) ou `grow`.
|
|
64
|
+
|
|
65
|
+
## Tema e janela
|
|
66
|
+
|
|
67
|
+
- Cores e tamanhos padrão em `theme` (`theme.text.secondary`, `theme.accent.base`,
|
|
68
|
+
`theme.danger.base`, `theme.divider`...). Mude `theme` antes de `run()` para personalizar.
|
|
69
|
+
- `windowControls.close()`, `.minimize()`, `.toggleMaximize()`, `.setTitle(t)`;
|
|
70
|
+
`windowState.width()`, `.height()`, `.maximized()`, `.activated()` são accessors.
|
|
71
|
+
- Ainda não existem: tema escuro, animações, quebra de linha, seleção de texto, IME,
|
|
72
|
+
várias janelas.
|
package/template/AGENTS.md
CHANGED
|
@@ -14,16 +14,21 @@ App nodebuilder: UI nativa para Linux em TSX, **sem DOM e sem React**. Leia ante
|
|
|
14
14
|
`background={() => (ativo() ? "#3584e4" : "#0000")}`.
|
|
15
15
|
- Listas: `<For each={lista}>{(item) => ...}</For>` (chave por referência). Condicional:
|
|
16
16
|
`<Show when={() => ...} fallback={...}>`.
|
|
17
|
+
- Estilo são props planas (`padding`, `gap`, `grow`, `background`...), não CSS. Não existem
|
|
18
|
+
`className`, `style={{}}`, `div`, `useState` nem bibliotecas de React (ex.: `lucide-react`).
|
|
17
19
|
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- Estilo são props planas, não CSS: `padding`, `gap`, `grow`, `align`, `justify`, `width`,
|
|
22
|
-
`height`, `background`, `radius`, `borderWidth`, `borderColor`, `color`, `fontSize`, `bold`.
|
|
23
|
-
Layout é flexbox; `align` padrão é `stretch`. `color`, `fontSize` e `bold` são herdados.
|
|
24
|
-
- Cores padrão em `theme` (`theme.text.secondary`, `theme.accent.base`...).
|
|
25
|
-
- Não existem `className`, `style={{}}`, `div`, `span`, `onChange` de DOM nem `useState`.
|
|
20
|
+
## Skills (detalhes por assunto)
|
|
21
|
+
Em `.agents/skills/<nome>/SKILL.md` (com links em `.claude/skills/` para o Claude Code). Agentes
|
|
22
|
+
que suportam skills as carregam sozinhos; os outros devem ler a skill do assunto antes de começar:
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
| Skill | Quando |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `nodebuilder-ui` | telas, widgets, props de estilo, layout, `For`/`Show`, tema |
|
|
27
|
+
| `nodebuilder-sem-react` | antes de instalar bibliotecas ou portar código React/web |
|
|
28
|
+
| `nodebuilder-imagens` | imagens (PNG, JPEG, WebP, AVIF), arquivos no app |
|
|
29
|
+
| `nodebuilder-icones` | ícones (Lucide, Phosphor) e SVG com `Icon` |
|
|
30
|
+
| `nodebuilder-graficos` | gráficos prontos (`@nodebuilderjs/core/charts`, API do Recharts) |
|
|
31
|
+
| `nodebuilder-desenho` | desenho livre com `Canvas` |
|
|
32
|
+
| `nodebuilder-testes` | testes sem janela (`mount`, `findByText`, `click`) |
|
|
33
|
+
| `nodebuilder-empacotar` | AppImage/.deb/.rpm, arquivos embutidos, ícone e app id |
|
|
34
|
+
| `nodebuilder-desempenho` | lentidão, listas grandes, dados ao vivo, animação |
|