create-nodebuilderjs 0.2.1 → 0.3.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.
Files changed (34) hide show
  1. package/index.js +12 -10
  2. package/package.json +2 -2
  3. package/template/.agents/skills/nodebuilder-charts/SKILL.md +87 -0
  4. package/template/.agents/skills/nodebuilder-drawing/SKILL.md +89 -0
  5. package/template/.agents/skills/nodebuilder-from-react/SKILL.md +80 -0
  6. package/template/.agents/skills/nodebuilder-icons/SKILL.md +95 -0
  7. package/template/.agents/skills/nodebuilder-images/SKILL.md +56 -0
  8. package/template/.agents/skills/nodebuilder-packaging/SKILL.md +37 -0
  9. package/template/.agents/skills/nodebuilder-performance/SKILL.md +59 -0
  10. package/template/.agents/skills/nodebuilder-testing/SKILL.md +199 -0
  11. package/template/.agents/skills/nodebuilder-ui/SKILL.md +535 -58
  12. package/template/AGENTS.md +34 -27
  13. package/template/README.md +19 -18
  14. package/template/_gitignore +1 -0
  15. package/template/src/App.tsx +16 -13
  16. package/template/src/components/Activity.tsx +24 -0
  17. package/template/src/components/Counter.tsx +24 -0
  18. package/template/src/components/Tasks.tsx +54 -0
  19. package/template/src/icons.ts +10 -0
  20. package/template/src/state.ts +50 -0
  21. package/template/test/App.test.tsx +45 -35
  22. package/template/.agents/skills/nodebuilder-desempenho/SKILL.md +0 -36
  23. package/template/.agents/skills/nodebuilder-desenho/SKILL.md +0 -54
  24. package/template/.agents/skills/nodebuilder-empacotar/SKILL.md +0 -37
  25. package/template/.agents/skills/nodebuilder-graficos/SKILL.md +0 -87
  26. package/template/.agents/skills/nodebuilder-icones/SKILL.md +0 -53
  27. package/template/.agents/skills/nodebuilder-imagens/SKILL.md +0 -50
  28. package/template/.agents/skills/nodebuilder-sem-react/SKILL.md +0 -48
  29. package/template/.agents/skills/nodebuilder-testes/SKILL.md +0 -56
  30. package/template/src/componentes/Atividade.tsx +0 -24
  31. package/template/src/componentes/Contador.tsx +0 -22
  32. package/template/src/componentes/Tarefas.tsx +0 -38
  33. package/template/src/estado.ts +0 -50
  34. package/template/src/icones.ts +0 -11
@@ -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`.
@@ -1,24 +0,0 @@
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
- }
@@ -1,22 +0,0 @@
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
- }
@@ -1,38 +0,0 @@
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
- }
@@ -1,50 +0,0 @@
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>;
@@ -1,11 +0,0 @@
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"/>');