create-nodebuilderjs 0.2.1 → 0.2.5
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 +10 -10
- package/package.json +2 -2
- package/template/.agents/skills/nodebuilder-charts/SKILL.md +87 -0
- package/template/.agents/skills/nodebuilder-drawing/SKILL.md +54 -0
- package/template/.agents/skills/nodebuilder-from-react/SKILL.md +69 -0
- package/template/.agents/skills/nodebuilder-icons/SKILL.md +53 -0
- package/template/.agents/skills/nodebuilder-images/SKILL.md +50 -0
- package/template/.agents/skills/nodebuilder-packaging/SKILL.md +37 -0
- package/template/.agents/skills/nodebuilder-performance/SKILL.md +40 -0
- package/template/.agents/skills/nodebuilder-testing/SKILL.md +97 -0
- package/template/.agents/skills/nodebuilder-ui/SKILL.md +282 -55
- package/template/AGENTS.md +30 -27
- package/template/README.md +18 -18
- 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 +11 -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/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// bun create nodebuilderjs [dir] [--title "
|
|
2
|
+
// bun create nodebuilderjs [dir] [--title "My App"] [--no-install] [--no-git]
|
|
3
3
|
// Plain JS (not TS) so it runs under both `bun create` and `npm create`.
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, symlinkSync, writeFileSync } from "node:fs";
|
|
@@ -22,19 +22,19 @@ const { values: opts, positionals } = parseArgs({
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
if (opts.help) {
|
|
25
|
-
console.log(`
|
|
25
|
+
console.log(`usage: bun create nodebuilderjs [folder] [--title "My App"] [--no-install] [--no-git]`);
|
|
26
26
|
process.exit(0);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
let dir = positionals[0];
|
|
30
30
|
if (!dir) {
|
|
31
31
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
32
|
-
dir = (await rl.question("
|
|
32
|
+
dir = (await rl.question("Project name: (my-app) ")).trim() || "my-app";
|
|
33
33
|
rl.close();
|
|
34
34
|
}
|
|
35
35
|
const target = resolve(dir);
|
|
36
36
|
if (existsSync(target) && readdirSync(target).length) {
|
|
37
|
-
console.error(`
|
|
37
|
+
console.error(`The folder ${dir} already exists and is not empty.`);
|
|
38
38
|
process.exit(1);
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -84,8 +84,8 @@ const run = (cmd, args) => spawnSync(cmd, args, { cwd: target, stdio: "inherit"
|
|
|
84
84
|
const hasBun = spawnSync("bun", ["--version"], { stdio: "ignore" }).status === 0;
|
|
85
85
|
|
|
86
86
|
if (!opts["no-install"]) {
|
|
87
|
-
if (!hasBun) console.warn("Bun
|
|
88
|
-
else if (!run("bun", ["install"])) console.warn("`bun install`
|
|
87
|
+
if (!hasBun) console.warn("Bun not found: install it from https://bun.sh and run `bun install`.");
|
|
88
|
+
else if (!run("bun", ["install"])) console.warn("`bun install` failed; run it again inside the folder.");
|
|
89
89
|
}
|
|
90
90
|
if (!opts["no-git"] && spawnSync("git", ["--version"], { stdio: "ignore" }).status === 0) {
|
|
91
91
|
run("git", ["init", "-q"]);
|
|
@@ -93,10 +93,10 @@ if (!opts["no-git"] && spawnSync("git", ["--version"], { stdio: "ignore" }).stat
|
|
|
93
93
|
|
|
94
94
|
const cd = relative(process.cwd(), target) || ".";
|
|
95
95
|
console.log(`
|
|
96
|
-
|
|
96
|
+
Done! ${title} created in ${cd}
|
|
97
97
|
|
|
98
98
|
cd ${cd}
|
|
99
|
-
bun run dev #
|
|
100
|
-
bun test #
|
|
101
|
-
bun run package # AppImage, .deb, .rpm
|
|
99
|
+
bun run dev # opens the window and reloads on save
|
|
100
|
+
bun test # tests without a window
|
|
101
|
+
bun run package # AppImage, .deb, .rpm and .tar.gz in dist/
|
|
102
102
|
`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nodebuilderjs",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.5",
|
|
4
|
+
"description": "Creates a nodebuilder app: bun create nodebuilderjs my-app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-charts
|
|
3
|
+
description: Line, area, bar, composed and pie/donut charts in nodebuilder with @nodebuilderjs/core/charts, which uses the same API as Recharts. Use when building dashboards, reports, time series, category comparisons or share breakdowns (pie), and whenever you would reach for Recharts, Chart.js or Victory.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Charts with `@nodebuilderjs/core/charts`
|
|
7
|
+
|
|
8
|
+
Already ships with `@nodebuilderjs/core` (install nothing); it only ends up in the app if imported.
|
|
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 [sales] = createSignal([
|
|
17
|
+
{ month: "Jan", revenue: 120, target: 100 },
|
|
18
|
+
{ month: "Feb", revenue: 150, target: 110 },
|
|
19
|
+
{ month: "Mar", revenue: 90, target: 120 },
|
|
20
|
+
]);
|
|
21
|
+
const channels = [{ name: "Store", value: 42 }, { name: "Website", value: 31 }];
|
|
22
|
+
|
|
23
|
+
export const charts = () => (
|
|
24
|
+
<>
|
|
25
|
+
<LineChart data={sales} height={240}>
|
|
26
|
+
<CartesianGrid vertical={false} />
|
|
27
|
+
<XAxis dataKey="month" />
|
|
28
|
+
<YAxis tickFormatter={(v) => `$${v}`} />
|
|
29
|
+
<Line dataKey="revenue" name="Revenue" />
|
|
30
|
+
<Line dataKey="target" name="Target" stroke="#e5a50a" type="monotone" dot={false} />
|
|
31
|
+
<Legend />
|
|
32
|
+
</LineChart>
|
|
33
|
+
<BarChart data={sales} height={200}>
|
|
34
|
+
<XAxis dataKey="month" />
|
|
35
|
+
<YAxis />
|
|
36
|
+
<Bar dataKey="revenue" fill={theme.accent.base} radius={3} />
|
|
37
|
+
</BarChart>
|
|
38
|
+
<AreaChart data={sales} height={160}>
|
|
39
|
+
<YAxis domain={[0, 200]} />
|
|
40
|
+
<Area dataKey="revenue" type="monotone" />
|
|
41
|
+
</AreaChart>
|
|
42
|
+
<PieChart height={220}>
|
|
43
|
+
<Pie data={channels} dataKey="value" nameKey="name" innerRadius="50%" paddingAngle={2}>
|
|
44
|
+
<Cell fill="#3584e4" />
|
|
45
|
+
<Cell fill="#26a269" />
|
|
46
|
+
</Pie>
|
|
47
|
+
<Legend />
|
|
48
|
+
</PieChart>
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## What is supported
|
|
54
|
+
|
|
55
|
+
| Component | Props |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `LineChart`, `AreaChart`, `BarChart`, `ComposedChart` (mixed) | `data` (array or signal), `height` (240), `width` or `grow`, and the style props |
|
|
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 or `(value) => text`) |
|
|
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 or `"80%"`), `paddingAngle` (degrees), `label` (true or `({ name, value, percent }) => text`); `<Cell fill>` per slice |
|
|
65
|
+
| `Tooltip` | `formatter(value, name)`, `labelFormatter(category)`; guide line at the category under the mouse (on a pie: the slice) |
|
|
66
|
+
| `Legend` | no props (bottom, centered) |
|
|
67
|
+
|
|
68
|
+
`dataKey` is the field name or a function `(row) => value`.
|
|
69
|
+
|
|
70
|
+
## Differences from Recharts
|
|
71
|
+
|
|
72
|
+
- **No `ResponsiveContainer`:** the chart already fills the parent's width; give it a `height`.
|
|
73
|
+
- Series with the same `stackId` stack (bars in a single column, areas on top of each other);
|
|
74
|
+
the y axis accounts for the totals.
|
|
75
|
+
- The x axis drops labels that would overlap, keeping the last one (like `interval="preserveEnd"`).
|
|
76
|
+
- Not available yet: `Brush`, `ReferenceLine`, `ScatterChart`, `RadarChart`, secondary
|
|
77
|
+
axes, animations; on `Line`, `label`.
|
|
78
|
+
- Pies start at the top and go clockwise (`startAngle`/`endAngle` do not exist); the
|
|
79
|
+
labels sit outside the slices, so keep `outerRadius` around 70% for them to fit.
|
|
80
|
+
- Colors: without `stroke`/`fill`, each series takes the next palette color (the first is
|
|
81
|
+
`theme.accent.base`).
|
|
82
|
+
- For anything beyond this, draw with `<Canvas>` (skill `nodebuilder-drawing`).
|
|
83
|
+
|
|
84
|
+
## Performance
|
|
85
|
+
|
|
86
|
+
Changing the `data` signal redraws only that chart. Updating every frame (live data
|
|
87
|
+
with `requestAnimationFrame`) works; hundreds of points per series are fine.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-drawing
|
|
3
|
+
description: Freeform drawing in nodebuilder with the Canvas widget (lines, polygons, arcs, gauges, sparklines, annotations, custom visualizations). Use for non-rectangular shapes and charts that @nodebuilderjs/core/charts does not cover, instead of the browser <canvas> or D3. For regular line, area, bar and pie charts, use the nodebuilder-charts skill.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Drawing with `<Canvas>`
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import { Canvas, createSignal, theme } from "@nodebuilderjs/core";
|
|
10
|
+
|
|
11
|
+
const [data, setData] = createSignal([12, 40, 33, 70, 55, 90]);
|
|
12
|
+
|
|
13
|
+
<Canvas height={160} draw={(g) => {
|
|
14
|
+
const v = data(); // signals read here redraw the 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"); // grid
|
|
18
|
+
const pts = v.flatMap((n, i) => [x(i), y(n)]);
|
|
19
|
+
g.polygon([0, g.height, ...pts, g.width, g.height], "#3584e433"); // area
|
|
20
|
+
g.polyline(pts, theme.accent.base, 2); // line
|
|
21
|
+
g.circle(x(v.length - 1), y(v[v.length - 1]!), 4, theme.accent.base);
|
|
22
|
+
g.text("max 100", 4, 0, theme.text.secondary, 11);
|
|
23
|
+
}} />
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## API (`g`)
|
|
27
|
+
|
|
28
|
+
| Method | What |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `g.width`, `g.height` | canvas size (inside the `padding`) |
|
|
31
|
+
| `rect(x, y, w, h, color, radius?)` | rectangle (bars) |
|
|
32
|
+
| `line(x0, y0, x1, y1, color, width = 1, cap = "round")` | segment; cap `round`, `butt` (flat) or `square` |
|
|
33
|
+
| `polyline(points, color, width = 1, { cap, closed })` | line through several points `[x0, y0, x1, y1, ...]`, round joins |
|
|
34
|
+
| `polygon(points, color)` | filled polygon (concave is fine; self-intersecting is not drawn) |
|
|
35
|
+
| `circle(cx, cy, radius, color)` | disc |
|
|
36
|
+
| `arc(cx, cy, outerRadius, innerRadius, start, sweep, color)` | slice (inner 0) or ring; radians, 0 = top, clockwise |
|
|
37
|
+
| `text(text, x, y, color, size?, bold?)`, `measureText(text, size?)` | labels; `y` is the top of the line |
|
|
38
|
+
|
|
39
|
+
## Rules
|
|
40
|
+
|
|
41
|
+
- Give the canvas a size: `width`/`height` or `grow`. Without a size, `draw` does not run.
|
|
42
|
+
- Local coordinates, with `y` pointing down. Flip it for charts: `g.height - value * scale`.
|
|
43
|
+
- `draw` must only draw: do not create signals or effects or call `set` inside it.
|
|
44
|
+
- The canvas redraws when a signal read in `draw` changes or its size changes; the geometry
|
|
45
|
+
is recorded, so other parts of the window redrawing cost nothing here.
|
|
46
|
+
- Live data: update the signal with `requestAnimationFrame`; thousands of points
|
|
47
|
+
work (5000 line + area points at ~120 fps).
|
|
48
|
+
- Pie/donut: accumulate the slices in radians (`fraction * 2 * Math.PI`); a small gap
|
|
49
|
+
(`- 0.03` on the sweep) separates the slices visually.
|
|
50
|
+
- Translucent colors on `polyline` darken at the joins: prefer opaque colors for lines
|
|
51
|
+
and translucent ones for areas.
|
|
52
|
+
- Interaction (tooltip, clicking a point, dragging): use `onPointerMove`/`onClick` on the
|
|
53
|
+
`Canvas` itself; `e.localX`/`e.localY` are in the same coordinates as the drawing (the `padding`
|
|
54
|
+
offsets them: subtract it).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-from-react
|
|
3
|
+
description: Translation of React, DOM, CSS and web library habits to nodebuilder, and which npm packages work. Use before installing any UI, icon, component, state or styling library (lucide-react, MUI, Tailwind, styled-components, react-*, solid-js...), when writing useState/useEffect/className/div/onChange, or when porting React/Solid/HTML code to this app.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Coming from React (or the web)
|
|
7
|
+
|
|
8
|
+
nodebuilder draws the interface with the GPU, without a browser: **there is no DOM, CSS, React
|
|
9
|
+
or Solid**. The JSX looks the same as always, but the elements are the widgets from
|
|
10
|
+
`@nodebuilderjs/core`.
|
|
11
|
+
|
|
12
|
+
## Direct translation
|
|
13
|
+
|
|
14
|
+
| On the web / React | In nodebuilder |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `useState(0)` | `const [n, setN] = createSignal(0)`; read with `n()` |
|
|
17
|
+
| `useMemo` | `createMemo(() => ...)` |
|
|
18
|
+
| `useEffect(() => ..., [deps])` | `createEffect(() => ...)` (automatic deps) + `onCleanup` |
|
|
19
|
+
| component re-render | does not exist: the component runs **once** |
|
|
20
|
+
| `{count}` in JSX | `{count}` (the function, not `count()`), otherwise it stays fixed |
|
|
21
|
+
| `style={{ padding: 8 }}`, `className` | props: `padding={8}`, `background="#fff"` |
|
|
22
|
+
| `div`, `span`, `p`, `h1` | `Column`, `Row`, `Box`, `Text fontSize={24} bold` |
|
|
23
|
+
| input `onChange` | `TextField value={v} onInput={setV}` |
|
|
24
|
+
| `<input type="range">`, `type="radio"`, MUI switch, `<progress>` | `Slider`, `RadioGroup`, `Toggle`, `ProgressBar`/`Spinner` |
|
|
25
|
+
| `<select>`, Dialog/Modal, Tabs, Tooltip (MUI, Radix, shadcn) | `Select`, `Dialog`, `Tabs` (+ `<Switch>` for the content), `Tooltip` |
|
|
26
|
+
| `disabled` on input/button | `disabled` on any control (value or function) |
|
|
27
|
+
| `list.map(...)` in JSX | `<For each={list}>{(item) => ...}</For>` |
|
|
28
|
+
| `list.map((x, i) => ...)` with `key={i}` (list of primitives) | `<Index each={list}>{(item, i) => ...}</Index>`; `item` is an accessor (`item()`), `i` is a number |
|
|
29
|
+
| `cond && <X/>` | `<Show when={() => cond()}><X/></Show>` |
|
|
30
|
+
| `cond ? <A/> : <B/>`, `switch` that returns JSX | `<Show when={...} fallback={<B/>}><A/></Show>`; several cases: `<Switch fallback={...}><Match when={() => s() === "a"}>...</Match></Switch>` |
|
|
31
|
+
| `user && <X name={user.name}/>` | `<Show when={user}>{(u) => <X name={u().name} />}</Show>` (same as in Solid) |
|
|
32
|
+
| error boundary (`componentDidCatch`, `react-error-boundary`) | `<ErrorBoundary fallback={(error, reset) => ...}>` (as in Solid); catches errors on creation and in effects, not in `onClick` code |
|
|
33
|
+
| `createContext` / `useContext` | the same: `createContext(default)`, `<Ctx.Provider value={...}>`, `useContext(Ctx)`; a value that changes goes as an accessor |
|
|
34
|
+
| `useRef` + `ref={r}` / `el.focus()` | `ref={(el) => ...}` on any widget: `el.focus()`, `el.blur()`, `el.scrollTo(y)`, `el.width` |
|
|
35
|
+
| `overflow-y: auto` / `overflow-x: auto` / `overflow: auto` | `<Scroll>` / `<Scroll horizontal>` / `<Scroll horizontal vertical>` (with `grow={1}` or a size) |
|
|
36
|
+
| `el.scrollIntoView()`, `el.scrollTo({ left, top })`, `scrollLeft`/`scrollTop` | `el.scrollIntoView()` (scrolls the minimum, on the next frame), `el.scrollTo({ x, y })`, `el.scrollX`/`el.scrollY` |
|
|
37
|
+
| `min-height: 0` / `min-width: 0` around what scrolls | unnecessary: the content of a `Scroll` does not hold up the size of its container |
|
|
38
|
+
| `ResizeObserver`, media queries | `onResize={(s) => setWidth(s.width)}` + signal |
|
|
39
|
+
| `createPortal`, `position: fixed`, `z-index`, library modal/dropdown/tooltip | `<Overlay>` (`modal`, `anchor={() => el}`, `onDismiss`); skill `nodebuilder-ui` |
|
|
40
|
+
| CSS `opacity`, `transform: translate(...)` | props `opacity`, `translateX`, `translateY` (no `scale`/`rotate`) |
|
|
41
|
+
| `transition`, framer-motion, react-spring, `AnimatePresence` | `createTween`, `createPresence` (+ `<Show when={p.mounted}>`), `animate` |
|
|
42
|
+
| `prefers-color-scheme`, `ThemeProvider` with a dark theme | `setTheme("dark")`, `colorScheme()`, colors in functions: `() => theme.text.primary` |
|
|
43
|
+
| `white-space: nowrap` / `text-overflow: ellipsis` / `-webkit-line-clamp` | `wrap={false}` / `maxLines={1}` / `maxLines={n}` |
|
|
44
|
+
| CSS `text-align`, `line-height` | `textAlign` (`start`/`center`/`end`), `lineHeight` (multiplier) |
|
|
45
|
+
| CSS `:hover` | `onPointerEnter`/`onPointerLeave` + signal |
|
|
46
|
+
| selectable text (the default on the web) | only with `<Text selectable>`; without the prop it does not select (like `user-select: none`) |
|
|
47
|
+
| `navigator.clipboard.writeText/readText` | `clipboard.writeText(t)` / `await clipboard.readText()` |
|
|
48
|
+
| `fetch`, `fs`, timers | they work (Bun/Node APIs); update signals with the result |
|
|
49
|
+
|
|
50
|
+
## npm libraries
|
|
51
|
+
|
|
52
|
+
- **Work:** pure logic without the DOM (date-fns, zod, lodash, HTTP clients, Bun's SQLite,
|
|
53
|
+
parsers). Plain SVG icons with the `Icon` widget: `lucide-static`, `lucide` (the data,
|
|
54
|
+
not `createIcons()`) and `@phosphor-icons/core` (skill `nodebuilder-icons`).
|
|
55
|
+
- **Do not work:** anything that renders React/Solid/Vue or touches the DOM: `lucide-react`,
|
|
56
|
+
`lucide-solid`, `@phosphor-icons/react`, MUI, Chakra, shadcn/ui, Radix, Tailwind,
|
|
57
|
+
styled-components, react-router, browser canvas/SVG charting libraries.
|
|
58
|
+
- Charts: use `@nodebuilderjs/core/charts` (same API as Recharts; skill
|
|
59
|
+
`nodebuilder-charts`) or draw with `<Canvas>` (skill `nodebuilder-drawing`). Recharts,
|
|
60
|
+
Chart.js and D3 need the DOM and do not work.
|
|
61
|
+
- When in doubt: if the package imports `react`, `react-dom`, `solid-js/web` or uses `document`,
|
|
62
|
+
it will not work here.
|
|
63
|
+
|
|
64
|
+
## Common mistakes
|
|
65
|
+
|
|
66
|
+
- Calling `useContext` inside a handler or `setTimeout`: it only works in the component body.
|
|
67
|
+
- Reading the signal in the component body (`const x = count();`) and expecting it to update.
|
|
68
|
+
- Destructuring reactive props (`const { value } = props`) and losing the function.
|
|
69
|
+
- Putting `createSignal` inside an effect to "reset" state: create it outside and use `set`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-icons
|
|
3
|
+
description: Icons and SVG in nodebuilder with the Icon widget, using Lucide (lucide-static or lucide) and Phosphor (@phosphor-icons/core). Use when adding icons to buttons, menus, sidebars or lists, when choosing an icon library, or when showing any .svg file.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Icons
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
bun add lucide-static # Lucide .svg files
|
|
10
|
+
bun add lucide # or: the same icons as data (import { Camera } from "lucide")
|
|
11
|
+
bun add @phosphor-icons/core # Phosphor: assets/<weight>/<name>.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 [active] = createSignal(false);
|
|
21
|
+
|
|
22
|
+
<Icon svg={camera} /> // 20 px, color of the surrounding text
|
|
23
|
+
<Icon svg={heart} size={32} color="#e01b24" />
|
|
24
|
+
<Icon svg={Star} size={16} strokeWidth={1.5} /> // stroke width (outline icons)
|
|
25
|
+
<Button variant="primary"><Icon svg={Star} size={16} /><Text>Favorite</Text></Button>
|
|
26
|
+
<Row color={() => (active() ? theme.accent.base : theme.text.secondary)}>
|
|
27
|
+
<Icon svg={camera} /><Text>Camera</Text> {/* icon and text change color together */}
|
|
28
|
+
</Row>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Rules
|
|
32
|
+
|
|
33
|
+
- **Never** use `lucide-react`, `lucide-solid`, `@phosphor-icons/react`, `react-icons` or
|
|
34
|
+
`@heroicons/react`: they require React/DOM. Use the packages above.
|
|
35
|
+
- Import `.svg` files with `with { type: "text" }` (the import becomes the SVG text and
|
|
36
|
+
`nodebuilder package` embeds it). `with { type: "file" }` (path) also works.
|
|
37
|
+
- `svg` accepts: SVG text, a path to a `.svg`, or data from the `lucide` package. It can be
|
|
38
|
+
a function (`svg={() => (open() ? chevronUp : chevronDown)}`).
|
|
39
|
+
- `size` is the height in logical px (default 20, `theme.icon.size`); the width follows the
|
|
40
|
+
`viewBox`. The color comes from `color` on the `Icon` itself or is inherited from a parent, as with `Text`.
|
|
41
|
+
- Changing the color is free (single-color icons are tinted on the GPU); changing the size
|
|
42
|
+
rasterizes again (~0.5 ms). They stay sharp on HiDPI screens.
|
|
43
|
+
- Phosphor weights: `thin`, `light`, `regular`, `bold`, `fill`, `duotone` (folders in
|
|
44
|
+
`assets/`; names carry the weight suffix, except `regular`: `heart.svg`, `heart-fill.svg`).
|
|
45
|
+
|
|
46
|
+
## SVG limits
|
|
47
|
+
|
|
48
|
+
Supported: `path` (with arcs and curves), `rect`, `circle`, `ellipse`, `line`, `polyline`,
|
|
49
|
+
`polygon`, `g`, `transform`, `fill`/`stroke` (colors, `currentColor`), stroke width, caps,
|
|
50
|
+
joins, `fill-rule`, opacities and inline `style=""`.
|
|
51
|
+
Not supported (the rest of the drawing still shows): gradients, patterns, text, filters, masks,
|
|
52
|
+
clip paths, `<use>`, dashes and `<style>`. For logos and illustrations like that: export a 2x PNG and
|
|
53
|
+
use `Image`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-images
|
|
3
|
+
description: Show images (PNG, JPEG, WebP, AVIF, HEIC) with nodebuilder's Image widget and include the files in the packaged app. Use when adding photos, logos, avatars, thumbnails or any image file to the UI. For icons and SVG, use the nodebuilder-icons skill.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Images
|
|
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} /> // height follows the aspect ratio
|
|
13
|
+
<Image src={photo} width={200} height={120} fit="cover" radius={12} />
|
|
14
|
+
<Image src={() => avatarOf(user())} width={32} height={32} radius={16} opacity={0.9} />
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Rules
|
|
18
|
+
|
|
19
|
+
- **Import files with `with { type: "file" }`.** The import becomes the file path, and
|
|
20
|
+
`nodebuilder package` embeds the image in the executable. `new URL("./x.png", import.meta.url)`
|
|
21
|
+
and bare paths work in `bun run dev`, but **break in the packaged app**.
|
|
22
|
+
- `src` accepts: a path (relative to the current directory), a `file:` `URL`, the file bytes
|
|
23
|
+
(`Uint8Array`, e.g. from a `fetch`) or pixels `{ width, height, data }` (8-bit RGBA).
|
|
24
|
+
It can be a function: changing `src` releases the old image.
|
|
25
|
+
- Size: without `width`/`height`, it uses the file's pixels (1 pixel = 1 logical px). With only one
|
|
26
|
+
of the two, the other follows the aspect ratio. For HiDPI screens, use a 2x larger file and fix the
|
|
27
|
+
size (`width={64}` for a 128 px PNG).
|
|
28
|
+
- `fit`: `contain` (default, whole image centered), `cover` (fills and crops), `fill`
|
|
29
|
+
(stretches). `radius` rounds the corners; `opacity` goes from 0 to 1.
|
|
30
|
+
- The same file used in several places is decoded once; the memory is released when the
|
|
31
|
+
last `Image` leaves the screen.
|
|
32
|
+
- A file that fails to load: a warning in the console and nothing drawn (the app does not crash).
|
|
33
|
+
|
|
34
|
+
## Formats
|
|
35
|
+
|
|
36
|
+
| Format | How | Notes |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| PNG, JPEG, WebP | decoded in TypeScript | always work; JPEG honors the EXIF orientation |
|
|
39
|
+
| AVIF, HEIC | `libheif` system library | without it, only these fail ("needs libheif"); the `.deb`/`.rpm` package suggests it but does not install it. Prefer WebP/PNG/JPEG for the app's own images |
|
|
40
|
+
| SVG | through the `Icon` widget (skill `nodebuilder-icons`) | subset for icons; illustrations with gradients/text: export a 2x PNG |
|
|
41
|
+
| Animated GIF, video | not supported | |
|
|
42
|
+
|
|
43
|
+
## Caveats
|
|
44
|
+
|
|
45
|
+
- Decoding is synchronous: a 4-megapixel photo takes ~0.1 s and freezes the UI
|
|
46
|
+
meanwhile. Shrink large images before putting them in the app (e.g. thumbnails).
|
|
47
|
+
- Images scaled down a lot on screen look jagged (no mipmaps): use files close to the
|
|
48
|
+
displayed size.
|
|
49
|
+
- In `bun test`, `Image` works without a window (textures are simulated); AVIF/HEIC
|
|
50
|
+
need the native addon and libheif.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-packaging
|
|
3
|
+
description: Build and distribute the nodebuilder app (AppImage, .deb, .rpm, .tar.gz) with nodebuilder package, bundle files (images, data) into the executable and configure name, icon and app id. Use when preparing a release to install, when changing the app's name/icon, or when the packaged app behaves differently from bun run dev.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Packaging
|
|
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/icon.svg --version 1.2.0
|
|
11
|
+
bunx nodebuilder package --help
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Produces in `dist/` a single executable (Bun + app + native core, ~31 MB compressed) inside
|
|
15
|
+
an AppImage, `.deb`, `.rpm` and `.tar.gz`, with a menu entry and icon. Runs on distros with glibc
|
|
16
|
+
2.34+ (Ubuntu 22.04+, Debian 12+, Fedora, RHEL 9+).
|
|
17
|
+
|
|
18
|
+
## Rules
|
|
19
|
+
|
|
20
|
+
- **App files** (images, JSON, sounds) are bundled into the executable only if imported with
|
|
21
|
+
`with { type: "file" }` (the import becomes the path). `readFileSync("./data.json")` with a
|
|
22
|
+
loose path works in dev and **fails in the package**. For JSON, `import data from
|
|
23
|
+
"./data.json"` also bundles it.
|
|
24
|
+
- The `<Window>`'s `appId` must match `--app-id` (the template already does this); otherwise the
|
|
25
|
+
icon and taskbar grouping do not work.
|
|
26
|
+
- `--icon`: `.svg` or a 256x256 PNG. `--name` is the executable/package name (lowercase).
|
|
27
|
+
- System libraries (Wayland, X11, EGL, FreeType, HarfBuzz, fontconfig) are declared
|
|
28
|
+
dependencies of the packages, never bundled. `libheif` (only for AVIF/HEIC) is
|
|
29
|
+
only suggested.
|
|
30
|
+
- User data: save it in `~/.local/share/<app>` (`process.env.XDG_DATA_HOME`), never
|
|
31
|
+
next to the executable (`/usr/bin` is not writable).
|
|
32
|
+
|
|
33
|
+
## Verify
|
|
34
|
+
|
|
35
|
+
- Run the generated executable outside the project folder (`cd / && path/to/app`) to catch
|
|
36
|
+
files that only existed on the development disk.
|
|
37
|
+
- AppImage: `chmod +x dist/*.AppImage && ./dist/*.AppImage`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-performance
|
|
3
|
+
description: Measure and improve the performance of nodebuilder apps (fps, CPU, memory) and avoid slow patterns in large lists, frequent updates and animations. Use when the app is slow, stuttering or freezing, using too much CPU/memory, or when building lists with hundreds of items, live data or animations.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Performance
|
|
7
|
+
|
|
8
|
+
## Measure
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
NB_STATS=1 bun src/main.tsx # per second: fps, ms of state/layout/paint/native, nodes, memory
|
|
12
|
+
NB_MAX_FPS=60 bun src/main.tsx # fps cap (default 120); also run(app, { maxFps: 60 })
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
When idle, the app draws nothing (0% CPU). State changes schedule a frame; several changes
|
|
16
|
+
before it go out together in the same frame.
|
|
17
|
+
|
|
18
|
+
## Patterns
|
|
19
|
+
|
|
20
|
+
- **Large lists:** always inside `<Scroll>`; whatever is outside the visible area is not
|
|
21
|
+
positioned or drawn. Even so, each item becomes nodes: with thousands, filter or paginate.
|
|
22
|
+
- **`<For>` is keyed by reference:** recreating the objects (`list.map((x) => ({ ...x }))`)
|
|
23
|
+
destroys and recreates every row. Keep the references and change only what changed; for
|
|
24
|
+
fields that change often, store a signal inside the item (`{ id, cpu: createSignal(0) }`).
|
|
25
|
+
If the data always arrives as new objects (a JSON every second) or is primitive,
|
|
26
|
+
use `<Index>`: rows stay by position and only the `item()` accessor changes.
|
|
27
|
+
- **Update the minimum:** one signal per changing value, and thin functions in props
|
|
28
|
+
(`text={() => fmt(item.cpu())}`) instead of one giant signal holding all the state.
|
|
29
|
+
- **Several changes together:** `batch(() => { setA(1); setB(2); })` produces a single frame and
|
|
30
|
+
runs the effects once.
|
|
31
|
+
- **Animation and live data:** to go from one value to another use `createTween`/`animate`; for
|
|
32
|
+
a loop, `requestAnimationFrame(cb)` (paced by the display), not a short `setInterval`. To
|
|
33
|
+
stop, do not request the next frame. `Spinner` and `ProgressBar` without a value request frames while
|
|
34
|
+
they are on screen: hide them when the wait is over.
|
|
35
|
+
- **Text:** measurements are cached; texts that change every frame (counters, clocks)
|
|
36
|
+
cost little, but avoid generating thousands of new strings per frame.
|
|
37
|
+
- **Images:** decoding is expensive and synchronous; reuse the same file (it is shared) and
|
|
38
|
+
use files at the displayed size.
|
|
39
|
+
- Heavy CPU work (parsing large files, computations) freezes the interface: split it into
|
|
40
|
+
chunks with `setTimeout(..., 0)` or use a `Worker`.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-testing
|
|
3
|
+
description: Write and debug tests for nodebuilder apps with bun test, without opening a window (mount, findByText, click, interact for mouse and keyboard, layout, debugTree). Use when creating tests, when verifying a behavior or layout change, or when a component test fails.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tests without a window
|
|
7
|
+
|
|
8
|
+
`bun test` runs the files in `test/`. Nothing opens a window: `mount` builds the tree with
|
|
9
|
+
fake text metrics (**8 px per character, 20 px line**), so layout is deterministic.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { expect, test } from "bun:test";
|
|
13
|
+
import { click, findAll, findByText, mount } from "@nodebuilderjs/core/testing";
|
|
14
|
+
import { debugTree } from "@nodebuilderjs/core";
|
|
15
|
+
import { App } from "../src/App.js";
|
|
16
|
+
|
|
17
|
+
test("adds a task", () => {
|
|
18
|
+
const { root, relayout, dispose } = mount(() => <App />, { width: 800, height: 600 });
|
|
19
|
+
click(findByText(root, "Add")!); // walks up to the parent with onClick (the Button)
|
|
20
|
+
expect(findByText(root, /1 task/)).toBeDefined();
|
|
21
|
+
relayout(); // recomputes positions after changes
|
|
22
|
+
const item = findByText(root, "New task")!;
|
|
23
|
+
expect(item.y).toBeGreaterThan(0);
|
|
24
|
+
dispose(); // disposes the app's effects and signals
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## API (`@nodebuilderjs/core/testing`)
|
|
29
|
+
|
|
30
|
+
- `mount(view, size?)` → `{ root, relayout, dispose }`. Layout runs once in `mount`;
|
|
31
|
+
call `relayout()` before checking `x`, `y`, `width`, `height` after changing state.
|
|
32
|
+
- `findByText(root, "text" | /regex/)`: first `Text` whose entire content matches.
|
|
33
|
+
- `findAll(root, (n) => ...)`: free-form search; `n.label` is `"Text \"...\""`, `"Button"`, `"Row"`...
|
|
34
|
+
- `click(node)`: fires the `onClick` of the node or of its nearest ancestor (quick shortcut).
|
|
35
|
+
- `interact(root)`: mouse and keyboard delivered as in the real app (hover, focus, capture
|
|
36
|
+
while dragging, Tab): `hover(n)`, `press(n)`, `release(n)`, `drag(n, dx, dy)`, `move(x, y)`,
|
|
37
|
+
`down(x, y)`/`up(x, y)` (button at a window position, e.g. on the scrollbar),
|
|
38
|
+
`scroll(n, dy, { dx, shift })`, `key("Return")`, `key("c", { ctrl: true })`,
|
|
39
|
+
`type("text")`, `focused` and `cursor` (the cursor the window would show).
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
const ui = interact(root);
|
|
43
|
+
ui.press(field); // focuses the TextField
|
|
44
|
+
ui.type("Ana"); // types into the focused field
|
|
45
|
+
ui.key("Return"); // fires onSubmit
|
|
46
|
+
ui.drag(slider, 80, 0); // drags 80 px to the right
|
|
47
|
+
```
|
|
48
|
+
- `debugTree(node)` (from `@nodebuilderjs/core`): tree with positions, great for inspecting layout.
|
|
49
|
+
- Animations finish immediately in tests (there is no window): a closed `Dialog` leaves the
|
|
50
|
+
tree at once. To test the animation, `const clock = frames()` after `mount()` and
|
|
51
|
+
`clock.advance(ms)` (16 ms frames).
|
|
52
|
+
- `ref={(el) => el.focus()}` (focus on creation) takes effect in a microtask: create
|
|
53
|
+
`interact(root)` and `await Promise.resolve()` before typing. `onResize` fires on every
|
|
54
|
+
`relayout()` in which the size changed.
|
|
55
|
+
- Scrolling: `n.scrollY`/`n.scrollX` give the position; `ui.scroll(n, 60)` uses the wheel,
|
|
56
|
+
`ui.key("Next")` (PageDown; PageUp is `"Prior"`) the keyboard. `scrollTo` and `scrollIntoView()`
|
|
57
|
+
apply on the next layout: call `relayout()` before checking.
|
|
58
|
+
- Overlays are positioned during layout: after opening or closing one, call `relayout()`.
|
|
59
|
+
Their content lives in `root.layers` (not in `root.children`); `findByText(root, ...)` searches
|
|
60
|
+
both.
|
|
61
|
+
|
|
62
|
+
## Clipboard and selection
|
|
63
|
+
|
|
64
|
+
In tests, `clipboard` is an in-memory area (no window, no system). To check a copy, read it
|
|
65
|
+
back; reading is asynchronous, so the test becomes `async`:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { expect, test } from "bun:test";
|
|
69
|
+
import { Column, Text, clipboard } from "@nodebuilderjs/core";
|
|
70
|
+
import { interact, mount } from "@nodebuilderjs/core/testing";
|
|
71
|
+
|
|
72
|
+
test("copies the id by selecting the text", async () => {
|
|
73
|
+
const { root } = mount(() => <Column align="start"><Text selectable>id-42</Text></Column>);
|
|
74
|
+
const ui = interact(root);
|
|
75
|
+
const id = root.children[0]!.children[0]!;
|
|
76
|
+
ui.press(id); // the click gives focus to the text
|
|
77
|
+
ui.release(id);
|
|
78
|
+
ui.key("a", { ctrl: true }); // Ctrl+A: everything (or ui.drag(id, dx, 0) for a range)
|
|
79
|
+
ui.key("c", { ctrl: true });
|
|
80
|
+
expect(await clipboard.readText()).toBe("id-42");
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
To test pasting into a `TextField`, write first with `clipboard.writeText(...)`, focus the
|
|
85
|
+
field, `ui.key("v", { ctrl: true })` and wait one loop turn
|
|
86
|
+
(`await new Promise((r) => setImmediate(r))`) before checking the value. The in-memory area
|
|
87
|
+
is shared within the file: write the expected value at the start of each test.
|
|
88
|
+
|
|
89
|
+
## Tips
|
|
90
|
+
|
|
91
|
+
- State changes immediately (signals are synchronous); there is no `await` or `act()` (the
|
|
92
|
+
exception is reading the clipboard, above).
|
|
93
|
+
- Prefer `interact` over calling handlers by hand: it also tests focus, hover and propagation.
|
|
94
|
+
After changes that move nodes, call `relayout()` before interacting again.
|
|
95
|
+
- Test layout calculations with fixed sizes (`width`, `height`) so they do not depend on the
|
|
96
|
+
fake metrics.
|
|
97
|
+
- Before finishing a task: `bun run typecheck && bun test`.
|