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.
- package/index.js +12 -10
- package/package.json +2 -2
- package/template/.agents/skills/nodebuilder-charts/SKILL.md +87 -0
- package/template/.agents/skills/nodebuilder-drawing/SKILL.md +89 -0
- package/template/.agents/skills/nodebuilder-from-react/SKILL.md +80 -0
- package/template/.agents/skills/nodebuilder-icons/SKILL.md +95 -0
- package/template/.agents/skills/nodebuilder-images/SKILL.md +56 -0
- package/template/.agents/skills/nodebuilder-packaging/SKILL.md +37 -0
- package/template/.agents/skills/nodebuilder-performance/SKILL.md +59 -0
- package/template/.agents/skills/nodebuilder-testing/SKILL.md +199 -0
- package/template/.agents/skills/nodebuilder-ui/SKILL.md +535 -58
- package/template/AGENTS.md +34 -27
- package/template/README.md +19 -18
- package/template/_gitignore +1 -0
- package/template/src/App.tsx +16 -13
- 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 +10 -0
- package/template/src/state.ts +50 -0
- package/template/test/App.test.tsx +45 -35
- 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
- package/template/src/componentes/Atividade.tsx +0 -24
- package/template/src/componentes/Contador.tsx +0 -22
- package/template/src/componentes/Tarefas.tsx +0 -38
- package/template/src/estado.ts +0 -50
- 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
|
-
}
|
package/template/src/estado.ts
DELETED
|
@@ -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>;
|
package/template/src/icones.ts
DELETED
|
@@ -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"/>');
|