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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nodebuilderjs",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Cria um app nodebuilder: bun create nodebuilderjs meu-app",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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+.
@@ -1,24 +1,32 @@
1
- import { Button, Column, Row, Text, Window, createSignal, theme } from "@nodebuilderjs/core";
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 [count, setCount] = createSignal(0);
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={480} height={360}>
9
- <Column grow={1} align="center" justify="center" gap={16} padding={24}>
10
- <Text fontSize={24} bold>
11
- {"{{title}}"}
12
- </Text>
13
- <Text color={theme.text.secondary}>{times}</Text>
14
- <Row gap={8}>
15
- <Button label="Zerar" variant="flat" onClick={() => setCount(0)} />
16
- <Button label="Clique aqui" variant="primary" onClick={() => setCount((c) => c + 1)} />
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
- <Text fontSize={12} color={theme.text.secondary}>
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 { click, findByText, mount } from "@nodebuilderjs/core/testing";
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
- click(findByText(root, "Clique aqui")!);
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
- click(findByText(root, "Zerar")!);
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
+ });