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
package/template/AGENTS.md
CHANGED
|
@@ -1,34 +1,41 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
nodebuilder app: native Linux UI in TSX, **no DOM and no React**. Read this before changing code.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
- `bun run dev`
|
|
7
|
-
-
|
|
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
|
-
##
|
|
10
|
-
- **
|
|
11
|
-
|
|
12
|
-
- **
|
|
13
|
-
`<Text>{count()}</Text>`
|
|
14
|
-
`background={() => (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 (
|
|
21
|
-
|
|
22
|
-
|
|
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 |
|
|
31
|
+
| Skill | When |
|
|
25
32
|
|---|---|
|
|
26
|
-
| `nodebuilder-ui` |
|
|
27
|
-
| `nodebuilder-
|
|
28
|
-
| `nodebuilder-
|
|
29
|
-
| `nodebuilder-
|
|
30
|
-
| `nodebuilder-
|
|
31
|
-
| `nodebuilder-
|
|
32
|
-
| `nodebuilder-
|
|
33
|
-
| `nodebuilder-
|
|
34
|
-
| `nodebuilder-
|
|
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`) |
|
package/template/README.md
CHANGED
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
# {{title}}
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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 #
|
|
8
|
-
bun start #
|
|
9
|
-
bun 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
|
|
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
|
-
##
|
|
15
|
+
## Structure
|
|
15
16
|
|
|
16
17
|
```
|
|
17
18
|
src/
|
|
18
|
-
main.tsx
|
|
19
|
-
App.tsx layout
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
28
|
+
App.test.tsx tests without a window: mount + interact (click, type)
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
To remove the example and start from scratch, keep only the `<Window>` in `App.tsx`.
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
Requirements: Linux with Wayland or X11, [Bun](https://bun.sh) 1.4+.
|
package/template/_gitignore
CHANGED
package/template/src/App.tsx
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
import { Column, Icon, Row, Scroll, Text, Window, theme } from "@nodebuilderjs/core";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
|
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
|
-
|
|
15
|
-
<
|
|
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}>
|
|
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
|
-
<
|
|
23
|
-
<
|
|
25
|
+
<Counter state={state} />
|
|
26
|
+
<Activity state={state} />
|
|
24
27
|
</Column>
|
|
25
28
|
<Column grow={1}>
|
|
26
|
-
<
|
|
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
|
-
//
|
|
6
|
+
// Tests run without a window. interact() delivers mouse and keyboard like the real app.
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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("
|
|
24
|
-
const { root, relayout } =
|
|
25
|
-
|
|
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
|
-
//
|
|
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("
|
|
37
|
+
ui.type("Write a test");
|
|
37
38
|
ui.key("Return");
|
|
38
39
|
ui.key("Return");
|
|
39
40
|
relayout();
|
|
40
|
-
expect(findByText(root, "
|
|
41
|
-
expect(findByText(root, "3
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
expect(findByText(root, "2
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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`.
|