create-nodebuilderjs 0.2.0 → 0.2.1
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/package.json +1 -1
- package/template/README.md +18 -0
- package/template/src/App.tsx +25 -17
- package/template/src/componentes/Atividade.tsx +24 -0
- package/template/src/componentes/Contador.tsx +22 -0
- package/template/src/componentes/Tarefas.tsx +38 -0
- package/template/src/estado.ts +50 -0
- package/template/src/icones.ts +11 -0
- package/template/test/App.test.tsx +39 -4
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -11,4 +11,22 @@ bun run typecheck # TypeScript 7
|
|
|
11
11
|
bun run package # AppImage, .deb, .rpm e .tar.gz em dist/
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
## Estrutura
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
src/
|
|
18
|
+
main.tsx abre a janela: run(() => <App />)
|
|
19
|
+
App.tsx layout da tela; cria o estado e o passa aos componentes
|
|
20
|
+
estado.ts signals e ações do app (contador, tarefas, histórico)
|
|
21
|
+
icones.ts ícones SVG usados pelo app
|
|
22
|
+
componentes/
|
|
23
|
+
Contador.tsx botões e texto reativo
|
|
24
|
+
Tarefas.tsx campo de texto, lista com For/Checkbox, Show
|
|
25
|
+
Atividade.tsx gráfico (@nodebuilderjs/core/charts) que acompanha o estado
|
|
26
|
+
test/
|
|
27
|
+
App.test.tsx testes sem janela: mount + interact (clicar, digitar)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Para apagar o exemplo e começar do zero, deixe só a `<Window>` em `App.tsx`.
|
|
31
|
+
|
|
14
32
|
Requisitos: Linux com Wayland ou X11, [Bun](https://bun.sh) 1.4+.
|
package/template/src/App.tsx
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Column, Icon, Row, Scroll, Text, Window, theme } from "@nodebuilderjs/core";
|
|
2
|
+
import { Atividade } from "./componentes/Atividade.js";
|
|
3
|
+
import { Contador } from "./componentes/Contador.js";
|
|
4
|
+
import { Tarefas } from "./componentes/Tarefas.js";
|
|
5
|
+
import { criarEstado } from "./estado.js";
|
|
6
|
+
import { brilho } from "./icones.js";
|
|
2
7
|
|
|
3
8
|
export function App() {
|
|
4
|
-
const
|
|
5
|
-
const times = () => `Você clicou ${count()} ${count() === 1 ? "vez" : "vezes"}`;
|
|
6
|
-
|
|
9
|
+
const estado = criarEstado();
|
|
7
10
|
return (
|
|
8
|
-
<Window title="{{title}}" appId="{{appId}}" width={
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
<Window title="{{title}}" appId="{{appId}}" width={720} height={760}>
|
|
12
|
+
<Scroll grow={1} padding={20} gap={16}>
|
|
13
|
+
<Row gap={12} align="center">
|
|
14
|
+
<Icon svg={brilho} size={32} color={theme.accent.base} />
|
|
15
|
+
<Column gap={2}>
|
|
16
|
+
<Text fontSize={22} bold>{"{{title}}"}</Text>
|
|
17
|
+
<Text color={theme.text.secondary}>Edite src/App.tsx e salve: o bun run dev reabre a janela.</Text>
|
|
18
|
+
</Column>
|
|
19
|
+
</Row>
|
|
20
|
+
<Row gap={16} align="start">
|
|
21
|
+
<Column grow={1} gap={16}>
|
|
22
|
+
<Contador estado={estado} />
|
|
23
|
+
<Atividade estado={estado} />
|
|
24
|
+
</Column>
|
|
25
|
+
<Column grow={1}>
|
|
26
|
+
<Tarefas estado={estado} />
|
|
27
|
+
</Column>
|
|
17
28
|
</Row>
|
|
18
|
-
|
|
19
|
-
Edite src/App.tsx e salve: o bun run dev reabre a janela.
|
|
20
|
-
</Text>
|
|
21
|
-
</Column>
|
|
29
|
+
</Scroll>
|
|
22
30
|
</Window>
|
|
23
31
|
);
|
|
24
32
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Card, Icon, Row, Text, theme } from "@nodebuilderjs/core";
|
|
2
|
+
import { Area, AreaChart, CartesianGrid, Legend, Line, Tooltip, YAxis } from "@nodebuilderjs/core/charts";
|
|
3
|
+
import type { Estado } from "../estado.js";
|
|
4
|
+
import { grafico } from "../icones.js";
|
|
5
|
+
|
|
6
|
+
export function Atividade(props: { estado: Estado }) {
|
|
7
|
+
return (
|
|
8
|
+
<Card gap={8}>
|
|
9
|
+
<Row gap={8} align="center" color={theme.text.secondary}>
|
|
10
|
+
<Icon svg={grafico} size={16} />
|
|
11
|
+
<Text bold>Atividade</Text>
|
|
12
|
+
</Row>
|
|
13
|
+
{/* data é um signal: o gráfico acompanha cada clique e cada tarefa. */}
|
|
14
|
+
<AreaChart data={props.estado.historico} height={180}>
|
|
15
|
+
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
|
16
|
+
<YAxis />
|
|
17
|
+
<Area dataKey="cliques" name="Cliques" type="monotone" />
|
|
18
|
+
<Line dataKey="pendentes" name="Pendentes" stroke="#e5a50a" dot={false} />
|
|
19
|
+
<Tooltip />
|
|
20
|
+
<Legend />
|
|
21
|
+
</AreaChart>
|
|
22
|
+
</Card>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Button, Card, Icon, Row, Text, theme } from "@nodebuilderjs/core";
|
|
2
|
+
import type { Estado } from "../estado.js";
|
|
3
|
+
import { ponteiro } from "../icones.js";
|
|
4
|
+
|
|
5
|
+
export function Contador(props: { estado: Estado }) {
|
|
6
|
+
const { cliques, clicar, zerar } = props.estado;
|
|
7
|
+
// Só funções são reativas: `vezes` é lida pelo Text sempre que `cliques` muda.
|
|
8
|
+
const vezes = () => `Você clicou ${cliques()} ${cliques() === 1 ? "vez" : "vezes"}`;
|
|
9
|
+
return (
|
|
10
|
+
<Card gap={12}>
|
|
11
|
+
<Row gap={8} align="center" color={theme.text.secondary}>
|
|
12
|
+
<Icon svg={ponteiro} size={16} />
|
|
13
|
+
<Text bold>Contador</Text>
|
|
14
|
+
</Row>
|
|
15
|
+
<Text fontSize={20}>{vezes}</Text>
|
|
16
|
+
<Row gap={8}>
|
|
17
|
+
<Button label="Zerar" variant="flat" onClick={zerar} />
|
|
18
|
+
<Button label="Clique aqui" variant="primary" onClick={clicar} />
|
|
19
|
+
</Row>
|
|
20
|
+
</Card>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Button, Card, Checkbox, Column, For, Icon, Row, Show, Spacer, Text, TextField, createSignal, theme } from "@nodebuilderjs/core";
|
|
2
|
+
import type { Estado } from "../estado.js";
|
|
3
|
+
import { lista, mais } from "../icones.js";
|
|
4
|
+
|
|
5
|
+
export function Tarefas(props: { estado: Estado }) {
|
|
6
|
+
const { tarefas, pendentes, adicionar, limparConcluidas } = props.estado;
|
|
7
|
+
const [texto, setTexto] = createSignal("");
|
|
8
|
+
const enviar = () => adicionar(texto()) && setTexto("");
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<Card gap={12}>
|
|
12
|
+
<Row gap={8} align="center" color={theme.text.secondary}>
|
|
13
|
+
<Icon svg={lista} size={16} />
|
|
14
|
+
<Text bold>Tarefas</Text>
|
|
15
|
+
<Spacer />
|
|
16
|
+
<Text fontSize={12}>{() => `${pendentes()} ${pendentes() === 1 ? "pendente" : "pendentes"}`}</Text>
|
|
17
|
+
</Row>
|
|
18
|
+
<Row gap={8}>
|
|
19
|
+
<TextField grow={1} value={texto} onInput={setTexto} onSubmit={enviar} placeholder="Nova tarefa" />
|
|
20
|
+
<Button variant="primary" onClick={enviar} disabled={() => !texto().trim()}>
|
|
21
|
+
<Icon svg={mais} size={16} />
|
|
22
|
+
<Text>Adicionar</Text>
|
|
23
|
+
</Button>
|
|
24
|
+
</Row>
|
|
25
|
+
<Column gap={8}>
|
|
26
|
+
{/* For é keyed por referência: cada tarefa mantém seu estado ao reordenar. */}
|
|
27
|
+
<For each={tarefas} fallback={<Text color={theme.text.secondary}>Nada por aqui.</Text>}>
|
|
28
|
+
{(t) => <Checkbox checked={t.feita} onChange={t.marcar} label={t.titulo} />}
|
|
29
|
+
</For>
|
|
30
|
+
</Column>
|
|
31
|
+
<Show when={() => tarefas().some((t) => t.feita())}>
|
|
32
|
+
<Row>
|
|
33
|
+
<Button label="Limpar concluídas" variant="flat" onClick={limparConcluidas} />
|
|
34
|
+
</Row>
|
|
35
|
+
</Show>
|
|
36
|
+
</Card>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createMemo, createSignal } from "@nodebuilderjs/core";
|
|
2
|
+
|
|
3
|
+
export interface Tarefa {
|
|
4
|
+
id: number;
|
|
5
|
+
titulo: string;
|
|
6
|
+
feita: () => boolean;
|
|
7
|
+
marcar: (feita: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* O estado do app em um lugar só: signals e ações. Cada <App /> cria o seu (os testes
|
|
12
|
+
* também), e os componentes o recebem por props.
|
|
13
|
+
*/
|
|
14
|
+
export function criarEstado() {
|
|
15
|
+
const [cliques, setCliques] = createSignal(0);
|
|
16
|
+
// Um ponto por mudança, para o gráfico de atividade.
|
|
17
|
+
const [historico, setHistorico] = createSignal<{ passo: number; cliques: number; pendentes: number }[]>([]);
|
|
18
|
+
const [tarefas, setTarefas] = createSignal<Tarefa[]>([]);
|
|
19
|
+
let proximoId = 1;
|
|
20
|
+
|
|
21
|
+
const pendentes = createMemo(() => tarefas().filter((t) => !t.feita()).length);
|
|
22
|
+
const registrar = () =>
|
|
23
|
+
setHistorico((h) => [...h.slice(-29), { passo: h[h.length - 1]!.passo + 1, cliques: cliques(), pendentes: pendentes() }]);
|
|
24
|
+
|
|
25
|
+
const novaTarefa = (titulo: string): Tarefa => {
|
|
26
|
+
const [feita, setFeita] = createSignal(false);
|
|
27
|
+
return { id: proximoId++, titulo, feita, marcar: (v) => (setFeita(v), registrar()) };
|
|
28
|
+
};
|
|
29
|
+
setTarefas([novaTarefa("Ler o README"), novaTarefa("Criar um componente")]);
|
|
30
|
+
setHistorico([{ passo: 0, cliques: 0, pendentes: pendentes() }]);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
cliques,
|
|
34
|
+
historico,
|
|
35
|
+
tarefas,
|
|
36
|
+
pendentes,
|
|
37
|
+
clicar: () => (setCliques((c) => c + 1), registrar()),
|
|
38
|
+
zerar: () => (setCliques(0), registrar()),
|
|
39
|
+
adicionar: (titulo: string) => {
|
|
40
|
+
const texto = titulo.trim();
|
|
41
|
+
if (!texto) return false;
|
|
42
|
+
setTarefas((ts) => [...ts, novaTarefa(texto)]);
|
|
43
|
+
registrar();
|
|
44
|
+
return true;
|
|
45
|
+
},
|
|
46
|
+
limparConcluidas: () => (setTarefas((ts) => ts.filter((t) => !t.feita())), registrar()),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type Estado = ReturnType<typeof criarEstado>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Ícones em SVG (desenhados para este template). Para mais ícones, `bun add lucide-static`
|
|
2
|
+
// e `import x from "lucide-static/icons/x.svg" with { type: "text" }`: veja a skill
|
|
3
|
+
// nodebuilder-icones.
|
|
4
|
+
const svg = (corpo: string) =>
|
|
5
|
+
`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${corpo}</svg>`;
|
|
6
|
+
|
|
7
|
+
export const brilho = 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 mais = svg('<path d="M12 5v14M5 12h14"/>');
|
|
9
|
+
export const lista = 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 grafico = svg('<path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/>');
|
|
11
|
+
export const ponteiro = svg('<path d="M5 3l14 8-6 2-3 6z"/>');
|
|
@@ -1,14 +1,49 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import { findAll, findByText, interact, mount } from "@nodebuilderjs/core/testing";
|
|
3
3
|
import { App } from "../src/App.js";
|
|
4
4
|
|
|
5
|
+
// Os testes rodam sem janela. interact() entrega mouse e teclado como no app de verdade.
|
|
6
|
+
|
|
5
7
|
test("o botão conta os cliques", () => {
|
|
6
|
-
const { root } = mount(() => <App />);
|
|
8
|
+
const { root, relayout } = mount(() => <App />);
|
|
9
|
+
const ui = interact(root);
|
|
7
10
|
expect(findByText(root, "Você clicou 0 vezes")).toBeDefined();
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
ui.press(findByText(root, "Clique aqui")!);
|
|
13
|
+
ui.release(findByText(root, "Clique aqui")!);
|
|
14
|
+
relayout();
|
|
10
15
|
expect(findByText(root, "Você clicou 1 vez")).toBeDefined();
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
ui.press(findByText(root, "Zerar")!);
|
|
18
|
+
ui.release(findByText(root, "Zerar")!);
|
|
19
|
+
relayout();
|
|
13
20
|
expect(findByText(root, "Você clicou 0 vezes")).toBeDefined();
|
|
14
21
|
});
|
|
22
|
+
|
|
23
|
+
test("adicionar, concluir e limpar tarefas", () => {
|
|
24
|
+
const { root, relayout } = mount(() => <App />, { width: 800, height: 900 });
|
|
25
|
+
const ui = interact(root);
|
|
26
|
+
const clique = (texto: string) => {
|
|
27
|
+
const alvo = findByText(root, texto)!;
|
|
28
|
+
ui.press(alvo);
|
|
29
|
+
ui.release(alvo);
|
|
30
|
+
relayout();
|
|
31
|
+
};
|
|
32
|
+
expect(findByText(root, "2 pendentes")).toBeDefined();
|
|
33
|
+
|
|
34
|
+
// Digita no campo e confirma com Enter; texto vazio não vira tarefa.
|
|
35
|
+
ui.press(findAll(root, (n) => n.label === "TextField")[0]!);
|
|
36
|
+
ui.type("Escrever um teste");
|
|
37
|
+
ui.key("Return");
|
|
38
|
+
ui.key("Return");
|
|
39
|
+
relayout();
|
|
40
|
+
expect(findByText(root, "Escrever um teste")).toBeDefined();
|
|
41
|
+
expect(findByText(root, "3 pendentes")).toBeDefined();
|
|
42
|
+
|
|
43
|
+
clique("Ler o README"); // o texto da tarefa também marca a caixa
|
|
44
|
+
expect(findByText(root, "2 pendentes")).toBeDefined();
|
|
45
|
+
|
|
46
|
+
clique("Limpar concluídas");
|
|
47
|
+
expect(findByText(root, "Ler o README")).toBeUndefined();
|
|
48
|
+
expect(findByText(root, "2 pendentes")).toBeDefined();
|
|
49
|
+
});
|