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,34 +1,41 @@
1
1
  # AGENTS.md
2
2
 
3
- App nodebuilder: UI nativa para Linux em TSX, **sem DOM e sem React**. Leia antes de mudar código.
3
+ nodebuilder app: native Linux UI in TSX, **no DOM and no React**. Read this before changing code.
4
4
 
5
- ## Comandos
6
- - `bun run dev` abre o app; `bun test` roda os testes sem janela; `bun run typecheck` checa tipos.
7
- - Antes de concluir: `bun run typecheck && bun test`.
5
+ ## Commands
6
+ - `bun run dev` opens the app; `bun test` runs the tests without a window; `bun run typecheck` checks types.
7
+ - `bun run icons:add play pause` adds icon imports to `src/icons.ts` (after `bun add lucide-static`).
8
+ - `bun run screenshot` renders the app to `screenshot.png` without a window (also with no
9
+ display server); add `-- --theme dark` or `-- --scale 2`.
10
+ - Before finishing: `bun run typecheck && bun test`. **After a visual change, take a screenshot
11
+ and look at it** (open the PNG): tests check boxes, not colors, alignment or clipping.
8
12
 
9
- ## Modelo (igual ao SolidJS, não ao React)
10
- - **Componentes rodam uma vez.** Não há re-render nem hooks. Estado é `createSignal`;
11
- derivados são `createMemo`; efeitos são `createEffect` (+ `onCleanup`).
12
- - **Só funções são reativas.** `<Text>{count}</Text>` ou `text={() => ...}` atualizam;
13
- `<Text>{count()}</Text>` é lido uma vez e nunca muda. O mesmo vale para estilo:
14
- `background={() => (ativo() ? "#3584e4" : "#0000")}`.
15
- - Listas: `<For each={lista}>{(item) => ...}</For>` (chave por referência). Condicional:
16
- `<Show when={() => ...} fallback={...}>`.
17
- - Estilo são props planas (`padding`, `gap`, `grow`, `background`...), não CSS. Não existem
18
- `className`, `style={{}}`, `div`, `useState` nem bibliotecas de React (ex.: `lucide-react`).
13
+ ## Model (like SolidJS, not React)
14
+ - **Components run once.** There is no re-render and there are no hooks. State is `createSignal`;
15
+ derived values are `createMemo`; effects are `createEffect` (+ `onCleanup`).
16
+ - **Only functions are reactive.** `<Text>{count}</Text>` or `text={() => ...}` update;
17
+ `<Text>{count()}</Text>` is read once and never changes. The same goes for styles:
18
+ `background={() => (active() ? "#3584e4" : "#0000")}`, and for theme colors:
19
+ `color={() => theme.text.secondary}` follows a switch to the dark theme.
20
+ - Lists: `<For each={list}>{(item) => ...}</For>` (keyed by reference) or
21
+ `<Index each={list}>{(item) => ...}</Index>` (by position; `item` is an accessor).
22
+ Conditions: `<Show when={() => ...} fallback={...}>`; several cases: `<Switch>` with
23
+ `<Match when={...}>`. Errors: `<ErrorBoundary fallback={(error, reset) => ...}>`.
24
+ - Styles are flat props (`padding`, `gap`, `grow`, `background`...), not CSS. There is no
25
+ `className`, `style={{}}`, `div` or `useState`, and React libraries (e.g. `lucide-react`) don't work.
19
26
 
20
- ## Skills (detalhes por assunto)
21
- Em `.agents/skills/<nome>/SKILL.md` (com links em `.claude/skills/` para o Claude Code). Agentes
22
- que suportam skills as carregam sozinhos; os outros devem ler a skill do assunto antes de começar:
27
+ ## Skills (details by topic)
28
+ In `.agents/skills/<name>/SKILL.md` (linked from `.claude/skills/` for Claude Code). Agents
29
+ that support skills load them on their own; others should read the topic's skill before starting:
23
30
 
24
- | Skill | Quando |
31
+ | Skill | When |
25
32
  |---|---|
26
- | `nodebuilder-ui` | telas, widgets, props de estilo, layout, `For`/`Show`, tema |
27
- | `nodebuilder-sem-react` | antes de instalar bibliotecas ou portar código React/web |
28
- | `nodebuilder-imagens` | imagens (PNG, JPEG, WebP, AVIF), arquivos no app |
29
- | `nodebuilder-icones` | ícones (Lucide, Phosphor) e SVG com `Icon` |
30
- | `nodebuilder-graficos` | gráficos prontos (`@nodebuilderjs/core/charts`, API do Recharts) |
31
- | `nodebuilder-desenho` | desenho livre com `Canvas` |
32
- | `nodebuilder-testes` | testes sem janela (`mount`, `findByText`, `click`) |
33
- | `nodebuilder-empacotar` | AppImage/.deb/.rpm, arquivos embutidos, ícone e app id |
34
- | `nodebuilder-desempenho` | lentidão, listas grandes, dados ao vivo, animação |
33
+ | `nodebuilder-ui` | screens, widgets (incl. custom title bar with `dragRegion`/`WindowControls`, `Toggle`, `RadioGroup`, `Slider`, `Select`, `Tabs`, `Tooltip`, `Dialog`, `ProgressBar`, `disabled`), style props (incl. gradient `background={{ linear, angle }}`), hover/press (`hoverStyle`, `pressStyle`, `createInteraction`), layout (`Grid` of equal columns, `wrap` rows of chips, `margin`, `aspectRatio`, equal columns in a row with `width={0} grow={1}`, `position="absolute"`, `zIndex`, `clip`, `shadow`, overflow warnings), events, own components, `For`/`Index`/`Show`/`Switch`, `ErrorBoundary`, scrolling (horizontal, scrollbars, keyboard, `scrollIntoView`), context, `ref` (focus, scroll), `onResize`, `Overlay` (dialog, menu, tooltip), `opacity`/`translate`, theme (light/dark, `setTheme`), animations (`createTween`, `animate`, `easing`, `mixColor`), selectable text, copy/paste |
34
+ | `nodebuilder-from-react` | before installing libraries or porting React/web code |
35
+ | `nodebuilder-images` | images (PNG, JPEG, WebP, AVIF), files in the app |
36
+ | `nodebuilder-icons` | icons (Lucide, Phosphor) and SVG with `Icon`; `bun run icons:add <names...>` writes their imports into `src/icons.ts` |
37
+ | `nodebuilder-charts` | ready-made charts (`@nodebuilderjs/core/charts`, Recharts API) |
38
+ | `nodebuilder-drawing` | freehand drawing with `Canvas` (reactive `draw`, dashed lines, gradient fills with `g.linearGradient`, text cut to a width with "…") |
39
+ | `nodebuilder-testing` | tests without a window (`mount`, `findByText`/`findAllByText`, `findByLabel` (`ariaLabel`), `click`, `interact`, hidden targets that throw, `layoutWarnings()`, real text metrics (`useRealTextMetrics`), `frames`, clipboard), screenshots (`nodebuilder screenshot`, `screenshot()`) |
40
+ | `nodebuilder-packaging` | AppImage/.deb/.rpm, embedded files, icon and app id |
41
+ | `nodebuilder-performance` | slowness, long lists, live data, animation, CPU use while idle (`NB_STATS`, `NB_TRACE=frames`) |
@@ -1,32 +1,33 @@
1
1
  # {{title}}
2
2
 
3
- App desktop nativo para Linux feito com [nodebuilder](https://www.npmjs.com/package/@nodebuilderjs/core):
4
- componentes TSX com reatividade estilo SolidJS, desenhados por um núcleo nativo (Wayland/X11 + GLES).
3
+ Native Linux desktop app made with [nodebuilder](https://www.npmjs.com/package/@nodebuilderjs/core):
4
+ TSX components with SolidJS-style reactivity, drawn by a native core (Wayland/X11 + GLES).
5
5
 
6
6
  ```sh
7
- bun run dev # abre a janela e reinicia ao salvar
8
- bun start # roda uma vez
9
- bun test # testes sem janela (test/)
7
+ bun run dev # opens the window and restarts on save
8
+ bun start # runs once
9
+ bun test # tests without a window (test/)
10
10
  bun run typecheck # TypeScript 7
11
- bun run package # AppImage, .deb, .rpm e .tar.gz em dist/
11
+ bun run screenshot # screenshot.png of the app, without opening a window
12
+ bun run package # AppImage, .deb, .rpm and .tar.gz in dist/
12
13
  ```
13
14
 
14
- ## Estrutura
15
+ ## Structure
15
16
 
16
17
  ```
17
18
  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
19
+ main.tsx opens the window: run(() => <App />)
20
+ App.tsx screen layout and dark mode toggle; creates the state and passes it down
21
+ state.ts app signals and actions (counter, tasks, history)
22
+ icons.ts SVG icons used by the app
23
+ components/
24
+ Counter.tsx buttons, reactive text and a tooltip
25
+ Tasks.tsx text field, list with For/Checkbox, Show and a confirmation Dialog
26
+ Activity.tsx chart (@nodebuilderjs/core/charts) that follows the state
26
27
  test/
27
- App.test.tsx testes sem janela: mount + interact (clicar, digitar)
28
+ App.test.tsx tests without a window: mount + interact (click, type)
28
29
  ```
29
30
 
30
- Para apagar o exemplo e começar do zero, deixe só a `<Window>` em `App.tsx`.
31
+ To remove the example and start from scratch, keep only the `<Window>` in `App.tsx`.
31
32
 
32
- Requisitos: Linux com Wayland ou X11, [Bun](https://bun.sh) 1.4+.
33
+ Requirements: Linux with Wayland or X11, [Bun](https://bun.sh) 1.4+.
@@ -1,2 +1,3 @@
1
1
  node_modules/
2
2
  dist/
3
+ screenshot.png
@@ -1,29 +1,32 @@
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";
1
+ import { Column, Icon, Row, Scroll, Spacer, Text, Toggle, Window, colorScheme, setTheme, theme } from "@nodebuilderjs/core";
2
+ import { Activity } from "./components/Activity.js";
3
+ import { Counter } from "./components/Counter.js";
4
+ import { Tasks } from "./components/Tasks.js";
5
+ import { sparkle } from "./icons.js";
6
+ import { createState } from "./state.js";
7
7
 
8
8
  export function App() {
9
- const estado = criarEstado();
9
+ const state = createState();
10
10
  return (
11
11
  <Window title="{{title}}" appId="{{appId}}" width={720} height={760}>
12
12
  <Scroll grow={1} padding={20} gap={16}>
13
13
  <Row gap={12} align="center">
14
- <Icon svg={brilho} size={32} color={theme.accent.base} />
15
- <Column gap={2}>
14
+ {/* Theme colors go inside functions, so they follow a theme switch. */}
15
+ <Icon svg={sparkle} size={32} color={() => theme.accent.base} />
16
+ <Column gap={2} grow={1}>
16
17
  <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
+ <Text color={() => theme.text.secondary}>Edit src/App.tsx and save: bun run dev reopens the window.</Text>
18
19
  </Column>
20
+ <Spacer size={8} />
21
+ <Toggle checked={() => colorScheme() === "dark"} onChange={(on) => setTheme(on ? "dark" : "light")} label="Dark" />
19
22
  </Row>
20
23
  <Row gap={16} align="start">
21
24
  <Column grow={1} gap={16}>
22
- <Contador estado={estado} />
23
- <Atividade estado={estado} />
25
+ <Counter state={state} />
26
+ <Activity state={state} />
24
27
  </Column>
25
28
  <Column grow={1}>
26
- <Tarefas estado={estado} />
29
+ <Tasks state={state} />
27
30
  </Column>
28
31
  </Row>
29
32
  </Scroll>
@@ -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 { chart } from "../icons.js";
4
+ import type { State } from "../state.js";
5
+
6
+ export function Activity(props: { state: State }) {
7
+ return (
8
+ <Card gap={8}>
9
+ <Row gap={8} align="center" color={() => theme.text.secondary}>
10
+ <Icon svg={chart} size={16} />
11
+ <Text bold>Activity</Text>
12
+ </Row>
13
+ {/* data is a signal: the chart follows every click and every task. */}
14
+ <AreaChart data={props.state.history} height={180}>
15
+ <CartesianGrid vertical={false} strokeDasharray="3 3" />
16
+ <YAxis />
17
+ <Area dataKey="clicks" name="Clicks" type="monotone" />
18
+ <Line dataKey="pending" name="Pending" stroke="#e5a50a" dot={false} />
19
+ <Tooltip />
20
+ <Legend />
21
+ </AreaChart>
22
+ </Card>
23
+ );
24
+ }
@@ -0,0 +1,24 @@
1
+ import { Button, Card, Icon, Row, Text, Tooltip, theme } from "@nodebuilderjs/core";
2
+ import { pointer } from "../icons.js";
3
+ import type { State } from "../state.js";
4
+
5
+ export function Counter(props: { state: State }) {
6
+ const { clicks, click, reset } = props.state;
7
+ // Only functions are reactive: Text reads `times` again whenever `clicks` changes.
8
+ const times = () => `You clicked ${clicks()} ${clicks() === 1 ? "time" : "times"}`;
9
+ return (
10
+ <Card gap={12}>
11
+ <Row gap={8} align="center" color={() => theme.text.secondary}>
12
+ <Icon svg={pointer} size={16} />
13
+ <Text bold>Counter</Text>
14
+ </Row>
15
+ <Text fontSize={20}>{times}</Text>
16
+ <Row gap={8}>
17
+ <Tooltip text="Back to zero">
18
+ <Button label="Reset" variant="flat" onClick={reset} disabled={() => clicks() === 0} />
19
+ </Tooltip>
20
+ <Button label="Click me" variant="primary" onClick={click} />
21
+ </Row>
22
+ </Card>
23
+ );
24
+ }
@@ -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,10 @@
1
+ // SVG icons (drawn for this template). For more: `bun add lucide-static`, then
2
+ // `bun run icons:add play pause` adds their imports below (see the nodebuilder-icons skill).
3
+ const svg = (body: string) =>
4
+ `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
5
+
6
+ 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"/>');
7
+ export const plus = svg('<path d="M12 5v14M5 12h14"/>');
8
+ 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"/>');
9
+ export const chart = svg('<path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/>');
10
+ 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,49 +1,59 @@
1
- import { expect, test } from "bun:test";
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { colorScheme, setTheme } from "@nodebuilderjs/core";
2
3
  import { findAll, findByText, interact, mount } from "@nodebuilderjs/core/testing";
3
4
  import { App } from "../src/App.js";
4
5
 
5
- // Os testes rodam sem janela. interact() entrega mouse e teclado como no app de verdade.
6
+ // Tests run without a window. interact() delivers mouse and keyboard like the real app.
6
7
 
7
- test("o botão conta os cliques", () => {
8
- const { root, relayout } = mount(() => <App />);
9
- const ui = interact(root);
10
- expect(findByText(root, "Você clicou 0 vezes")).toBeDefined();
8
+ afterEach(() => setTheme("light"));
11
9
 
12
- ui.press(findByText(root, "Clique aqui")!);
13
- ui.release(findByText(root, "Clique aqui")!);
14
- relayout();
15
- expect(findByText(root, "Você clicou 1 vez")).toBeDefined();
16
-
17
- ui.press(findByText(root, "Zerar")!);
18
- ui.release(findByText(root, "Zerar")!);
19
- relayout();
20
- expect(findByText(root, "Você clicou 0 vezes")).toBeDefined();
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();
21
29
  });
22
30
 
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();
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();
33
34
 
34
- // Digita no campo e confirma com Enter; texto vazio não vira tarefa.
35
+ // Type in the field and confirm with Enter; empty text does not become a task.
35
36
  ui.press(findAll(root, (n) => n.label === "TextField")[0]!);
36
- ui.type("Escrever um teste");
37
+ ui.type("Write a test");
37
38
  ui.key("Return");
38
39
  ui.key("Return");
39
40
  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();
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
+ });
45
54
 
46
- clique("Limpar concluídas");
47
- expect(findByText(root, "Ler o README")).toBeUndefined();
48
- expect(findByText(root, "2 pendentes")).toBeDefined();
55
+ test("the toggle switches to the dark theme", () => {
56
+ const { click } = setup();
57
+ click("Dark");
58
+ expect(colorScheme()).toBe("dark");
49
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`.