create-nodebuilderjs 0.2.0 → 0.2.5
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 +10 -10
- package/package.json +2 -2
- package/template/.agents/skills/nodebuilder-charts/SKILL.md +87 -0
- package/template/.agents/skills/nodebuilder-drawing/SKILL.md +54 -0
- package/template/.agents/skills/nodebuilder-from-react/SKILL.md +69 -0
- package/template/.agents/skills/nodebuilder-icons/SKILL.md +53 -0
- package/template/.agents/skills/nodebuilder-images/SKILL.md +50 -0
- package/template/.agents/skills/nodebuilder-packaging/SKILL.md +37 -0
- package/template/.agents/skills/nodebuilder-performance/SKILL.md +40 -0
- package/template/.agents/skills/nodebuilder-testing/SKILL.md +97 -0
- package/template/.agents/skills/nodebuilder-ui/SKILL.md +282 -55
- package/template/AGENTS.md +30 -27
- package/template/README.md +25 -7
- package/template/src/App.tsx +28 -17
- package/template/src/components/Activity.tsx +24 -0
- package/template/src/components/Counter.tsx +24 -0
- package/template/src/components/Tasks.tsx +54 -0
- package/template/src/icons.ts +11 -0
- package/template/src/state.ts +50 -0
- package/template/test/App.test.tsx +54 -9
- package/template/.agents/skills/nodebuilder-desempenho/SKILL.md +0 -36
- package/template/.agents/skills/nodebuilder-desenho/SKILL.md +0 -54
- package/template/.agents/skills/nodebuilder-empacotar/SKILL.md +0 -37
- package/template/.agents/skills/nodebuilder-graficos/SKILL.md +0 -87
- package/template/.agents/skills/nodebuilder-icones/SKILL.md +0 -53
- package/template/.agents/skills/nodebuilder-imagens/SKILL.md +0 -50
- package/template/.agents/skills/nodebuilder-sem-react/SKILL.md +0 -48
- package/template/.agents/skills/nodebuilder-testes/SKILL.md +0 -56
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button, Card, Checkbox, Column, Dialog, For, Icon, Row, Show, Spacer, Text, TextField, createSignal, theme,
|
|
3
|
+
} from "@nodebuilderjs/core";
|
|
4
|
+
import { list, plus } from "../icons.js";
|
|
5
|
+
import type { State } from "../state.js";
|
|
6
|
+
|
|
7
|
+
export function Tasks(props: { state: State }) {
|
|
8
|
+
const { tasks, pending, add, clearCompleted } = props.state;
|
|
9
|
+
const [text, setText] = createSignal("");
|
|
10
|
+
const [confirming, setConfirming] = createSignal(false);
|
|
11
|
+
const submit = () => add(text()) && setText("");
|
|
12
|
+
const done = () => tasks().filter((t) => t.done()).length;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Card gap={12}>
|
|
16
|
+
<Row gap={8} align="center" color={() => theme.text.secondary}>
|
|
17
|
+
<Icon svg={list} size={16} />
|
|
18
|
+
<Text bold>Tasks</Text>
|
|
19
|
+
<Spacer />
|
|
20
|
+
<Text fontSize={12}>{() => `${pending()} pending`}</Text>
|
|
21
|
+
</Row>
|
|
22
|
+
<Row gap={8}>
|
|
23
|
+
<TextField grow={1} value={text} onInput={setText} onSubmit={submit} placeholder="New task" />
|
|
24
|
+
<Button variant="primary" onClick={submit} disabled={() => !text().trim()}>
|
|
25
|
+
<Icon svg={plus} size={16} />
|
|
26
|
+
<Text>Add</Text>
|
|
27
|
+
</Button>
|
|
28
|
+
</Row>
|
|
29
|
+
<Column gap={8}>
|
|
30
|
+
{/* For is keyed by reference: each task keeps its state when the list is reordered. */}
|
|
31
|
+
<For each={tasks} fallback={<Text color={() => theme.text.secondary}>Nothing here.</Text>}>
|
|
32
|
+
{(t) => <Checkbox checked={t.done} onChange={t.mark} label={t.title} />}
|
|
33
|
+
</For>
|
|
34
|
+
</Column>
|
|
35
|
+
<Show when={() => done() > 0}>
|
|
36
|
+
<Row>
|
|
37
|
+
<Button label="Clear completed" variant="flat" onClick={() => setConfirming(true)} />
|
|
38
|
+
</Row>
|
|
39
|
+
</Show>
|
|
40
|
+
{/* A modal on top of the window: Escape or a click outside calls onClose. */}
|
|
41
|
+
<Dialog
|
|
42
|
+
open={confirming}
|
|
43
|
+
onClose={() => setConfirming(false)}
|
|
44
|
+
title="Clear completed tasks?"
|
|
45
|
+
actions={[
|
|
46
|
+
<Button label="Cancel" variant="flat" onClick={() => setConfirming(false)} />,
|
|
47
|
+
<Button label="Clear" variant="danger" onClick={() => (clearCompleted(), setConfirming(false))} />,
|
|
48
|
+
]}
|
|
49
|
+
>
|
|
50
|
+
<Text color={() => theme.text.secondary}>{() => `${done()} ${done() === 1 ? "task" : "tasks"} will be removed.`}</Text>
|
|
51
|
+
</Dialog>
|
|
52
|
+
</Card>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// SVG icons (drawn for this template). For more icons, `bun add lucide-static` and
|
|
2
|
+
// `import x from "lucide-static/icons/x.svg" with { type: "text" }`: see the
|
|
3
|
+
// nodebuilder-icons skill.
|
|
4
|
+
const svg = (body: string) =>
|
|
5
|
+
`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
|
|
6
|
+
|
|
7
|
+
export const sparkle = svg('<path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z"/><path d="M19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8z"/>');
|
|
8
|
+
export const plus = svg('<path d="M12 5v14M5 12h14"/>');
|
|
9
|
+
export const list = svg('<path d="M9 6h11M9 12h11M9 18h11"/><circle cx="4.5" cy="6" r="1"/><circle cx="4.5" cy="12" r="1"/><circle cx="4.5" cy="18" r="1"/>');
|
|
10
|
+
export const chart = svg('<path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/>');
|
|
11
|
+
export const pointer = svg('<path d="M5 3l14 8-6 2-3 6z"/>');
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createMemo, createSignal } from "@nodebuilderjs/core";
|
|
2
|
+
|
|
3
|
+
export interface Task {
|
|
4
|
+
id: number;
|
|
5
|
+
title: string;
|
|
6
|
+
done: () => boolean;
|
|
7
|
+
mark: (done: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The app state in one place: signals and actions. Each <App /> creates its own (so do the
|
|
12
|
+
* tests), and the components receive it through props.
|
|
13
|
+
*/
|
|
14
|
+
export function createState() {
|
|
15
|
+
const [clicks, setClicks] = createSignal(0);
|
|
16
|
+
// One point per change, for the activity chart.
|
|
17
|
+
const [history, setHistory] = createSignal<{ step: number; clicks: number; pending: number }[]>([]);
|
|
18
|
+
const [tasks, setTasks] = createSignal<Task[]>([]);
|
|
19
|
+
let nextId = 1;
|
|
20
|
+
|
|
21
|
+
const pending = createMemo(() => tasks().filter((t) => !t.done()).length);
|
|
22
|
+
const record = () =>
|
|
23
|
+
setHistory((h) => [...h.slice(-29), { step: h[h.length - 1]!.step + 1, clicks: clicks(), pending: pending() }]);
|
|
24
|
+
|
|
25
|
+
const newTask = (title: string): Task => {
|
|
26
|
+
const [done, setDone] = createSignal(false);
|
|
27
|
+
return { id: nextId++, title, done, mark: (v) => (setDone(v), record()) };
|
|
28
|
+
};
|
|
29
|
+
setTasks([newTask("Read the README"), newTask("Write a component")]);
|
|
30
|
+
setHistory([{ step: 0, clicks: 0, pending: pending() }]);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
clicks,
|
|
34
|
+
history,
|
|
35
|
+
tasks,
|
|
36
|
+
pending,
|
|
37
|
+
click: () => (setClicks((c) => c + 1), record()),
|
|
38
|
+
reset: () => (setClicks(0), record()),
|
|
39
|
+
add: (title: string) => {
|
|
40
|
+
const text = title.trim();
|
|
41
|
+
if (!text) return false;
|
|
42
|
+
setTasks((ts) => [...ts, newTask(text)]);
|
|
43
|
+
record();
|
|
44
|
+
return true;
|
|
45
|
+
},
|
|
46
|
+
clearCompleted: () => (setTasks((ts) => ts.filter((t) => !t.done())), record()),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type State = ReturnType<typeof createState>;
|
|
@@ -1,14 +1,59 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { colorScheme, setTheme } from "@nodebuilderjs/core";
|
|
3
|
+
import { findAll, findByText, interact, mount } from "@nodebuilderjs/core/testing";
|
|
3
4
|
import { App } from "../src/App.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
const { root } = mount(() => <App />);
|
|
7
|
-
expect(findByText(root, "Você clicou 0 vezes")).toBeDefined();
|
|
6
|
+
// Tests run without a window. interact() delivers mouse and keyboard like the real app.
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
expect(findByText(root, "Você clicou 1 vez")).toBeDefined();
|
|
8
|
+
afterEach(() => setTheme("light"));
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
function setup() {
|
|
11
|
+
const app = mount(() => <App />, { width: 800, height: 900 });
|
|
12
|
+
const ui = interact(app.root);
|
|
13
|
+
const click = (text: string) => {
|
|
14
|
+
const target = findByText(app.root, text)!;
|
|
15
|
+
ui.press(target);
|
|
16
|
+
ui.release(target);
|
|
17
|
+
app.relayout();
|
|
18
|
+
};
|
|
19
|
+
return { ...app, ui, click };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test("the button counts clicks", () => {
|
|
23
|
+
const { root, click } = setup();
|
|
24
|
+
expect(findByText(root, "You clicked 0 times")).toBeDefined();
|
|
25
|
+
click("Click me");
|
|
26
|
+
expect(findByText(root, "You clicked 1 time")).toBeDefined();
|
|
27
|
+
click("Reset");
|
|
28
|
+
expect(findByText(root, "You clicked 0 times")).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("add, complete and clear tasks, confirming in a dialog", () => {
|
|
32
|
+
const { root, ui, relayout, click } = setup();
|
|
33
|
+
expect(findByText(root, "2 pending")).toBeDefined();
|
|
34
|
+
|
|
35
|
+
// Type in the field and confirm with Enter; empty text does not become a task.
|
|
36
|
+
ui.press(findAll(root, (n) => n.label === "TextField")[0]!);
|
|
37
|
+
ui.type("Write a test");
|
|
38
|
+
ui.key("Return");
|
|
39
|
+
ui.key("Return");
|
|
40
|
+
relayout();
|
|
41
|
+
expect(findByText(root, "Write a test")).toBeDefined();
|
|
42
|
+
expect(findByText(root, "3 pending")).toBeDefined();
|
|
43
|
+
|
|
44
|
+
click("Read the README"); // the task's text also ticks the box
|
|
45
|
+
expect(findByText(root, "2 pending")).toBeDefined();
|
|
46
|
+
|
|
47
|
+
click("Clear completed");
|
|
48
|
+
expect(findByText(root, "1 task will be removed.")).toBeDefined();
|
|
49
|
+
click("Clear");
|
|
50
|
+
expect(findByText(root, "Read the README")).toBeUndefined();
|
|
51
|
+
expect(findByText(root, "Clear completed tasks?")).toBeUndefined();
|
|
52
|
+
expect(findByText(root, "2 pending")).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("the toggle switches to the dark theme", () => {
|
|
56
|
+
const { click } = setup();
|
|
57
|
+
click("Dark");
|
|
58
|
+
expect(colorScheme()).toBe("dark");
|
|
14
59
|
});
|
|
@@ -1,36 +0,0 @@
|
|
|
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`.
|
|
@@ -1,54 +0,0 @@
|
|
|
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).
|
|
@@ -1,37 +0,0 @@
|
|
|
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`.
|
|
@@ -1,87 +0,0 @@
|
|
|
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.
|
|
@@ -1,53 +0,0 @@
|
|
|
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`.
|
|
@@ -1,50 +0,0 @@
|
|
|
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.
|
|
@@ -1,48 +0,0 @@
|
|
|
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`.
|
|
@@ -1,56 +0,0 @@
|
|
|
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`.
|