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/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
|
|
|
@@ -73,6 +73,8 @@ const pkg = {
|
|
|
73
73
|
dev: "bun --watch src/main.tsx",
|
|
74
74
|
test: "bun test",
|
|
75
75
|
typecheck: "tsc --noEmit",
|
|
76
|
+
screenshot: "nodebuilder screenshot src/main.tsx screenshot.png",
|
|
77
|
+
"icons:add": "nodebuilder icons add --out src/icons.ts",
|
|
76
78
|
package: `nodebuilder package src/main.tsx --title "${title}" --app-id ${appId}`,
|
|
77
79
|
},
|
|
78
80
|
dependencies: { "@nodebuilderjs/core": `^${version}` },
|
|
@@ -84,8 +86,8 @@ const run = (cmd, args) => spawnSync(cmd, args, { cwd: target, stdio: "inherit"
|
|
|
84
86
|
const hasBun = spawnSync("bun", ["--version"], { stdio: "ignore" }).status === 0;
|
|
85
87
|
|
|
86
88
|
if (!opts["no-install"]) {
|
|
87
|
-
if (!hasBun) console.warn("Bun
|
|
88
|
-
else if (!run("bun", ["install"])) console.warn("`bun install`
|
|
89
|
+
if (!hasBun) console.warn("Bun not found: install it from https://bun.sh and run `bun install`.");
|
|
90
|
+
else if (!run("bun", ["install"])) console.warn("`bun install` failed; run it again inside the folder.");
|
|
89
91
|
}
|
|
90
92
|
if (!opts["no-git"] && spawnSync("git", ["--version"], { stdio: "ignore" }).status === 0) {
|
|
91
93
|
run("git", ["init", "-q"]);
|
|
@@ -93,10 +95,10 @@ if (!opts["no-git"] && spawnSync("git", ["--version"], { stdio: "ignore" }).stat
|
|
|
93
95
|
|
|
94
96
|
const cd = relative(process.cwd(), target) || ".";
|
|
95
97
|
console.log(`
|
|
96
|
-
|
|
98
|
+
Done! ${title} created in ${cd}
|
|
97
99
|
|
|
98
100
|
cd ${cd}
|
|
99
|
-
bun run dev #
|
|
100
|
-
bun test #
|
|
101
|
-
bun run package # AppImage, .deb, .rpm
|
|
101
|
+
bun run dev # opens the window and reloads on save
|
|
102
|
+
bun test # tests without a window
|
|
103
|
+
bun run package # AppImage, .deb, .rpm and .tar.gz in dist/
|
|
102
104
|
`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nodebuilderjs",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
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,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodebuilder-drawing
|
|
3
|
+
description: Freeform drawing in nodebuilder with the Canvas widget (lines, dashed lines, gradient fills, polygons, arcs, gauges, sparklines, annotations, labels cut to a width with an ellipsis, custom visualizations); the draw function is reactive. 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", dash?)` | segment; cap `round`, `butt` (flat) or `square`; `dash` like `[4, 3]` (dash, gap...) |
|
|
33
|
+
| `polyline(points, color, width = 1, { cap, closed, dash })` | 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?, { maxWidth, ellipsis, weight, italic, letterSpacing })` | one line of text; `y` is the top of the line. With `maxWidth`, text that does not fit is cut and ends in "…" (`ellipsis: false` just cuts; nothing is drawn if not even "…" fits). `weight` (1..1000) wins over `bold`. No wrapping |
|
|
38
|
+
| `measureText(text, size?, bold?, { weight, italic, letterSpacing })` | `{ width, height }` of that text, to align it |
|
|
39
|
+
| `linearGradient(x0, y0, x1, y1, stops)` | a gradient usable in place of `color` in `rect`, `line`, `polyline`, `polygon`, `circle`, `arc` (not `text`): offset 0 at `(x0, y0)`, 1 at `(x1, y1)`, end colors beyond; stops are colors or `[color, offset]` |
|
|
40
|
+
|
|
41
|
+
Gradient area under a line (fades out toward the bottom):
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { Canvas, createSignal, mixColor, theme } from "@nodebuilderjs/core";
|
|
45
|
+
|
|
46
|
+
const [points] = createSignal([0, 120, 80, 40, 160, 90, 240, 20]);
|
|
47
|
+
<Canvas height={160} draw={(g) => {
|
|
48
|
+
const pts = points();
|
|
49
|
+
const fade = g.linearGradient(0, 0, 0, g.height, [mixColor(theme.accent.base, "transparent", 0.6), "transparent"]);
|
|
50
|
+
g.polygon([pts[0]!, g.height, ...pts, pts[pts.length - 2]!, g.height], fade);
|
|
51
|
+
g.polyline(pts, theme.accent.base, 2);
|
|
52
|
+
}} />;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Sizes default to the theme's font size (14). Text that must fit a width:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { Canvas, createSignal, theme } from "@nodebuilderjs/core";
|
|
59
|
+
|
|
60
|
+
const [title] = createSignal("A very long track title that does not fit");
|
|
61
|
+
<Canvas width={180} height={40} draw={(g) => {
|
|
62
|
+
const { height } = g.measureText(title(), 13);
|
|
63
|
+
g.text(title(), 4, (g.height - height) / 2, theme.text.primary, 13, false, { maxWidth: g.width - 8 }); // "A very long track…", centered vertically
|
|
64
|
+
g.line(0, g.height - 1, g.width, g.height - 1, theme.divider, 1, "butt", [4, 3]); // dashed
|
|
65
|
+
}} />;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For wrapping paragraphs, a `<Text>` next to the canvas (or over it: an absolute sibling in a
|
|
69
|
+
`Box`, declared after the `Canvas`) is simpler than drawing text.
|
|
70
|
+
|
|
71
|
+
## Rules
|
|
72
|
+
|
|
73
|
+
- Give the canvas a size: `width`/`height` or `grow`. Without a size, `draw` does not run.
|
|
74
|
+
- Local coordinates, with `y` pointing down. Flip it for charts: `g.height - value * scale`.
|
|
75
|
+
- `draw` must only draw: do not create signals or effects or call `set` inside it.
|
|
76
|
+
- `draw` is **reactive**: it runs inside an effect, so every signal it reads (the data, a
|
|
77
|
+
hovered index, `theme.*` colors) is tracked, and changing one redraws the canvas, once per
|
|
78
|
+
change. It also redraws when its size changes. Do not wrap it in `createEffect` and do not
|
|
79
|
+
read the data outside `draw` (that reads it once). The geometry is recorded, so other parts
|
|
80
|
+
of the window redrawing cost nothing here.
|
|
81
|
+
- Live data: update the signal with `requestAnimationFrame`; thousands of points
|
|
82
|
+
work (5000 line + area points at ~120 fps).
|
|
83
|
+
- Pie/donut: accumulate the slices in radians (`fraction * 2 * Math.PI`); a small gap
|
|
84
|
+
(`- 0.03` on the sweep) separates the slices visually.
|
|
85
|
+
- Translucent colors on `polyline` darken at the joins: prefer opaque colors for lines
|
|
86
|
+
and translucent ones for areas.
|
|
87
|
+
- Interaction (tooltip, clicking a point, dragging): use `onPointerMove`/`onClick` on the
|
|
88
|
+
`Canvas` itself; `e.localX`/`e.localY` are in the same coordinates as the drawing (the `padding`
|
|
89
|
+
offsets them: subtract it).
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
| `z-index` among siblings (badge over a card's content) | `zIndex={1}` (siblings only: no global stacking context; ties keep child order) |
|
|
40
|
+
| `createPortal`, `position: fixed`, `z-index` above everything, library modal/dropdown/tooltip | `<Overlay>` (`modal`, `anchor={() => el}`, `onDismiss`); skill `nodebuilder-ui` |
|
|
41
|
+
| CSS `display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; row-gap: 12px` | `<Grid columns={4} gap={8} rowGap={12}>` (equal columns only; no spans or named areas) |
|
|
42
|
+
| CSS `margin: 8px 16px`, `margin-top`, negative margins | `margin={[8, 16]}`, `margin={[12, 0, 0, 0]}`, `margin={[0, -16]}` (no collapsing, no `auto`; adds to `gap`) |
|
|
43
|
+
| CSS `flex-wrap: wrap` (chips, tags), `row-gap` | `<Row wrap gap={6} rowGap={8}>` (lines start at the top: no `align-content`) |
|
|
44
|
+
| CSS `flex: 1 1 0` (equal or 1:1:2 columns in one row) | `width={0} grow={1}` (`grow={2}`...) on each child |
|
|
45
|
+
| CSS `aspect-ratio: 16 / 9`, square thumbnails | `aspectRatio={16 / 9}`; `aspectRatio={1}` in `Grid` cells gives square cells |
|
|
46
|
+
| CSS `align-self`, `position: absolute; inset: 0` / `right: 8px` | `alignSelf`, `position="absolute" left={0} top={0} right={0} bottom={0}` / `right={8}` (inside a `Scroll` it scrolls with the content) |
|
|
47
|
+
| CSS `background: linear-gradient(180deg, #1db954, #121212 60%)` | `background={{ linear: ["#1db954", ["#121212", 0.6]], angle: 180 }}` (linear only; on `<canvas>`: `g.linearGradient(x0, y0, x1, y1, stops)`) |
|
|
48
|
+
| CSS `opacity`, `transform: translate(...)` | props `opacity`, `translateX`, `translateY` (no `scale`/`rotate`) |
|
|
49
|
+
| `transition`, framer-motion, react-spring, `AnimatePresence` | `createTween`, `createPresence` (+ `<Show when={p.mounted}>`), `animate` |
|
|
50
|
+
| `prefers-color-scheme`, `ThemeProvider` with a dark theme | `setTheme("dark")`, `colorScheme()`, colors in functions: `() => theme.text.primary` |
|
|
51
|
+
| `white-space: nowrap` / `text-overflow: ellipsis` / `-webkit-line-clamp` | `wrap={false}` / `maxLines={1}` / `maxLines={n}` |
|
|
52
|
+
| CSS `text-align`, `line-height`, `text-decoration: underline` | `textAlign` (`start`/`center`/`end`), `lineHeight` (multiplier), `underline` (inherited) |
|
|
53
|
+
| CSS `font-weight: 600`, `font-weight: bold` | `fontWeight={600}`, `bold` (= 700); inherited |
|
|
54
|
+
| CSS `font-style: italic` | `italic` (inherited) |
|
|
55
|
+
| CSS `letter-spacing: 0.08em` / `2px` | `letterSpacing={2}` (px only: multiply `em` by the `fontSize`); inherited |
|
|
56
|
+
| CSS `:hover` / `:active`, `.card:hover .play` | `hoverStyle={{ background: "#..." }}` / `pressStyle={{...}}` on any widget; state read elsewhere: `const card = createInteraction()`, `<Box {...card.props}>`, `<Show when={card.hovered}>` |
|
|
57
|
+
| selectable text (the default on the web) | only with `<Text selectable>`; without the prop it does not select (like `user-select: none`) |
|
|
58
|
+
| `navigator.clipboard.writeText/readText` | `clipboard.writeText(t)` / `await clipboard.readText()` |
|
|
59
|
+
| `fetch`, `fs`, timers | they work (Bun/Node APIs); update signals with the result |
|
|
60
|
+
|
|
61
|
+
## npm libraries
|
|
62
|
+
|
|
63
|
+
- **Work:** pure logic without the DOM (date-fns, zod, lodash, HTTP clients, Bun's SQLite,
|
|
64
|
+
parsers). Plain SVG icons with the `Icon` widget: `lucide-static`, `lucide` (the data,
|
|
65
|
+
not `createIcons()`) and `@phosphor-icons/core` (skill `nodebuilder-icons`).
|
|
66
|
+
- **Do not work:** anything that renders React/Solid/Vue or touches the DOM: `lucide-react`,
|
|
67
|
+
`lucide-solid`, `@phosphor-icons/react`, MUI, Chakra, shadcn/ui, Radix, Tailwind,
|
|
68
|
+
styled-components, react-router, browser canvas/SVG charting libraries.
|
|
69
|
+
- Charts: use `@nodebuilderjs/core/charts` (same API as Recharts; skill
|
|
70
|
+
`nodebuilder-charts`) or draw with `<Canvas>` (skill `nodebuilder-drawing`). Recharts,
|
|
71
|
+
Chart.js and D3 need the DOM and do not work.
|
|
72
|
+
- When in doubt: if the package imports `react`, `react-dom`, `solid-js/web` or uses `document`,
|
|
73
|
+
it will not work here.
|
|
74
|
+
|
|
75
|
+
## Common mistakes
|
|
76
|
+
|
|
77
|
+
- Calling `useContext` inside a handler or `setTimeout`: it only works in the component body.
|
|
78
|
+
- Reading the signal in the component body (`const x = count();`) and expecting it to update.
|
|
79
|
+
- Destructuring reactive props (`const { value } = props`) and losing the function.
|
|
80
|
+
- Putting `createSignal` inside an effect to "reset" state: create it outside and use `set`.
|
|
@@ -0,0 +1,95 @@
|
|
|
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 (generate the imports with `nodebuilder icons add`), 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, Column, 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 { Heart, Play, Star } from "lucide";
|
|
19
|
+
import starFill from "@phosphor-icons/core/assets/fill/star-fill.svg" with { type: "text" };
|
|
20
|
+
|
|
21
|
+
const [active] = createSignal(false);
|
|
22
|
+
const [liked, setLiked] = createSignal(false);
|
|
23
|
+
|
|
24
|
+
<Column gap={8}>
|
|
25
|
+
<Icon svg={camera} /> {/* 20 px, color of the surrounding text */}
|
|
26
|
+
<Icon svg={heart} size={32} color="#e01b24" />
|
|
27
|
+
<Icon svg={Star} size={16} strokeWidth={1.5} /> {/* stroke width (outline icons) */}
|
|
28
|
+
<Button variant="primary"><Icon svg={Star} size={16} /><Text>Favorite</Text></Button>
|
|
29
|
+
<Icon svg={Play} fill /> {/* solid Lucide icon (icon color) */}
|
|
30
|
+
<Icon svg={Heart} fill={() => liked() && "#e01b24"} onClick={() => setLiked(!liked())} />
|
|
31
|
+
<Icon svg={starFill} /> {/* Phosphor ships solid icons as "fill" */}
|
|
32
|
+
<Row color={() => (active() ? theme.accent.base : theme.text.secondary)}>
|
|
33
|
+
<Icon svg={camera} /><Text>Camera</Text> {/* icon and text change color together */}
|
|
34
|
+
</Row>
|
|
35
|
+
</Column>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Adding icons: `nodebuilder icons add`
|
|
39
|
+
|
|
40
|
+
For more than one or two icons, **generate the imports** instead of writing them:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
bun run icons:add play pause heart arrow-left # lucide-static -> src/icons.ts
|
|
44
|
+
bun run icons:add heart-fill house --from @phosphor-icons/core # Phosphor: weight by suffix
|
|
45
|
+
nodebuilder icons list arrow # names that contain "arrow"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
<!-- no-check -->
|
|
49
|
+
```tsx
|
|
50
|
+
import { Icon } from "@nodebuilderjs/core";
|
|
51
|
+
import { arrowLeft, heartFill } from "./icons.js"; // camelCase of the file name
|
|
52
|
+
|
|
53
|
+
<Icon svg={arrowLeft} />;
|
|
54
|
+
<Icon svg={heartFill} color="#e01b24" />;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- Install the package first (`bun add lucide-static`). `--out <file>` picks another module.
|
|
58
|
+
- One sorted, deduplicated line per icon; the rest of `src/icons.ts` (hand-drawn icons) stays.
|
|
59
|
+
A name already used in the file is an error (rename yours, or pick another icon).
|
|
60
|
+
- An unknown name fails with close matches (`did you mean heart-crack?`) and writes nothing:
|
|
61
|
+
run `nodebuilder icons list <part>` to find the right one.
|
|
62
|
+
- Writing them by hand: `import x from "lucide-static/icons/x.svg" with { type: "text" };`.
|
|
63
|
+
**Never** `export { default as x } from "....svg" with { type: "text" }`: Bun drops the
|
|
64
|
+
attribute on re-exports, so `x` becomes the file's path, not its text (`Icon` happens to
|
|
65
|
+
read paths too, anything expecting SVG text breaks); `icons add` rewrites those lines into
|
|
66
|
+
`import x from "....svg" with { type: "text" }; export { x };`.
|
|
67
|
+
|
|
68
|
+
## Rules
|
|
69
|
+
|
|
70
|
+
- **Never** use `lucide-react`, `lucide-solid`, `@phosphor-icons/react`, `react-icons` or
|
|
71
|
+
`@heroicons/react`: they require React/DOM. Use the packages above.
|
|
72
|
+
- Import `.svg` files with `with { type: "text" }` (the import becomes the SVG text and
|
|
73
|
+
`nodebuilder package` embeds it). `with { type: "file" }` (path) also works.
|
|
74
|
+
- `svg` accepts: SVG text, a path to a `.svg`, or data from the `lucide` package. It can be
|
|
75
|
+
a function (`svg={() => (open() ? chevronUp : chevronDown)}`).
|
|
76
|
+
- `size` is the height in logical px (default 20, `theme.icon.size`); the width follows the
|
|
77
|
+
`viewBox`. The color comes from `color` on the `Icon` itself or is inherited from a parent, as with `Text`.
|
|
78
|
+
- Solid icons: Lucide is outline only; `fill` (`true` = the icon's color, or a color) makes
|
|
79
|
+
it solid, like Lucide's own `fill` prop: it replaces the root `<svg>`'s `fill="none"`, so the
|
|
80
|
+
shapes get filled and keep their stroke. A fill a shape or group sets itself (`none`
|
|
81
|
+
included) still wins. It can be a function. **Never** patch the SVG text
|
|
82
|
+
(`svg.replace('fill="none"', ...)`). Phosphor has ready-made solid icons in the `fill` weight.
|
|
83
|
+
- Changing the color is free (single-color icons are tinted on the GPU); changing the size
|
|
84
|
+
rasterizes again (~0.5 ms). They stay sharp on HiDPI screens.
|
|
85
|
+
- Phosphor weights: `thin`, `light`, `regular`, `bold`, `fill`, `duotone` (folders in
|
|
86
|
+
`assets/`; names carry the weight suffix, except `regular`: `heart.svg`, `heart-fill.svg`).
|
|
87
|
+
|
|
88
|
+
## SVG limits
|
|
89
|
+
|
|
90
|
+
Supported: `path` (with arcs and curves), `rect`, `circle`, `ellipse`, `line`, `polyline`,
|
|
91
|
+
`polygon`, `g`, `transform`, `fill`/`stroke` (colors, `currentColor`), stroke width, caps,
|
|
92
|
+
joins, `fill-rule`, opacities and inline `style=""`.
|
|
93
|
+
Not supported (the rest of the drawing still shows): gradients, patterns, text, filters, masks,
|
|
94
|
+
clip paths, `<use>`, dashes and `<style>`. For logos and illustrations like that: export a 2x PNG and
|
|
95
|
+
use `Image`.
|
|
@@ -0,0 +1,56 @@
|
|
|
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 { Column, Image, createSignal } from "@nodebuilderjs/core";
|
|
10
|
+
import logo from "./assets/logo.png" with { type: "file" };
|
|
11
|
+
import photo from "./assets/photo.jpg" with { type: "file" };
|
|
12
|
+
|
|
13
|
+
const [user] = createSignal({ name: "Ana", avatar: logo });
|
|
14
|
+
|
|
15
|
+
<Column gap={8}>
|
|
16
|
+
<Image src={logo} width={64} /> {/* height follows the aspect ratio */}
|
|
17
|
+
<Image src={photo} width={200} height={120} fit="cover" radius={12} />
|
|
18
|
+
<Image src={() => user().avatar} width={32} height={32} radius={16} opacity={0.9} />
|
|
19
|
+
</Column>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Rules
|
|
23
|
+
|
|
24
|
+
- **Import files with `with { type: "file" }`.** The import becomes the file path, and
|
|
25
|
+
`nodebuilder package` embeds the image in the executable. `new URL("./x.png", import.meta.url)`
|
|
26
|
+
and bare paths work in `bun run dev`, but **break in the packaged app**.
|
|
27
|
+
- `src` accepts: a path (relative to the current directory), a `file:` `URL`, the file bytes
|
|
28
|
+
(`Uint8Array`, e.g. from a `fetch`) or pixels `{ width, height, data }` (8-bit RGBA).
|
|
29
|
+
It can be a function: changing `src` releases the old image.
|
|
30
|
+
- Size: without `width`/`height`, it uses the file's pixels (1 pixel = 1 logical px). With only one
|
|
31
|
+
of the two, the other follows the aspect ratio. For HiDPI screens, use a 2x larger file and fix the
|
|
32
|
+
size (`width={64}` for a 128 px PNG). A shape of your own that follows the space:
|
|
33
|
+
`aspectRatio={1} fit="cover"` (square covers; in a `<Grid columns={4}>` they fill each cell).
|
|
34
|
+
- `fit`: `contain` (default, whole image centered), `cover` (fills and crops), `fill`
|
|
35
|
+
(stretches). `radius` rounds the corners; `opacity` goes from 0 to 1.
|
|
36
|
+
- The same file used in several places is decoded once; the memory is released when the
|
|
37
|
+
last `Image` leaves the screen.
|
|
38
|
+
- A file that fails to load: a warning in the console and nothing drawn (the app does not crash).
|
|
39
|
+
|
|
40
|
+
## Formats
|
|
41
|
+
|
|
42
|
+
| Format | How | Notes |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| PNG, JPEG, WebP | decoded in TypeScript | always work; JPEG honors the EXIF orientation |
|
|
45
|
+
| 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 |
|
|
46
|
+
| SVG | through the `Icon` widget (skill `nodebuilder-icons`) | subset for icons; illustrations with gradients/text: export a 2x PNG |
|
|
47
|
+
| Animated GIF, video | not supported | |
|
|
48
|
+
|
|
49
|
+
## Caveats
|
|
50
|
+
|
|
51
|
+
- Decoding is synchronous: a 4-megapixel photo takes ~0.1 s and freezes the UI
|
|
52
|
+
meanwhile. Shrink large images before putting them in the app (e.g. thumbnails).
|
|
53
|
+
- Images scaled down a lot on screen look jagged (no mipmaps): use files close to the
|
|
54
|
+
displayed size.
|
|
55
|
+
- In `bun test`, `Image` works without a window (textures are simulated); AVIF/HEIC
|
|
56
|
+
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,59 @@
|
|
|
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 (also while idle), 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, why, nodes, memory
|
|
12
|
+
NB_TRACE=frames bun src/main.tsx # per frame: what asked for it, with the top of its stack
|
|
13
|
+
NB_MAX_FPS=60 bun src/main.tsx # fps cap (default 120); also run(app, { maxFps: 60 })
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
When idle, the app draws nothing (0% CPU) and `NB_STATS` prints nothing. State changes
|
|
17
|
+
schedule a frame; several changes before it go out together in the same frame.
|
|
18
|
+
|
|
19
|
+
## The app never goes idle
|
|
20
|
+
|
|
21
|
+
`NB_STATS` shows why frames happen: `why: inv 3 ev 2 raf 0` counts frames requested by
|
|
22
|
+
`invalidate()` (a signal changed something on screen), by native events (resize, hover,
|
|
23
|
+
scroll) and by `requestAnimationFrame`/animations. While nothing moves on screen it should
|
|
24
|
+
print nothing at all. If lines keep coming, run with `NB_TRACE=frames` to see who asks:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
frame 41: inv [in frame]
|
|
28
|
+
at <anonymous> (src/components/clock.tsx:12:7)
|
|
29
|
+
at paintTree (node_modules/@nodebuilderjs/core/src/render/paint.ts:140:8) x3
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`[in frame]` means the request came while that frame was being laid out or painted: usually
|
|
33
|
+
code that runs there (an `onResize` handler, a custom `paint`) and writes a signal every
|
|
34
|
+
time, even when the value did not change. Write only when the value really changes. In
|
|
35
|
+
development, the app also warns once (`frame loop: ...`) after 30 such frames in a row.
|
|
36
|
+
|
|
37
|
+
## Patterns
|
|
38
|
+
|
|
39
|
+
- **Large lists:** always inside `<Scroll>`; whatever is outside the visible area is not
|
|
40
|
+
positioned or drawn. Even so, each item becomes nodes: with thousands, filter or paginate.
|
|
41
|
+
- **`<For>` is keyed by reference:** recreating the objects (`list.map((x) => ({ ...x }))`)
|
|
42
|
+
destroys and recreates every row. Keep the references and change only what changed; for
|
|
43
|
+
fields that change often, store a signal inside the item (`{ id, cpu: createSignal(0) }`).
|
|
44
|
+
If the data always arrives as new objects (a JSON every second) or is primitive,
|
|
45
|
+
use `<Index>`: rows stay by position and only the `item()` accessor changes.
|
|
46
|
+
- **Update the minimum:** one signal per changing value, and thin functions in props
|
|
47
|
+
(`text={() => fmt(item.cpu())}`) instead of one giant signal holding all the state.
|
|
48
|
+
- **Several changes together:** `batch(() => { setA(1); setB(2); })` produces a single frame and
|
|
49
|
+
runs the effects once.
|
|
50
|
+
- **Animation and live data:** to go from one value to another use `createTween`/`animate`; for
|
|
51
|
+
a loop, `requestAnimationFrame(cb)` (paced by the display), not a short `setInterval`. To
|
|
52
|
+
stop, do not request the next frame. `Spinner` and `ProgressBar` without a value request frames while
|
|
53
|
+
they are on screen: hide them when the wait is over.
|
|
54
|
+
- **Text:** measurements are cached; texts that change every frame (counters, clocks)
|
|
55
|
+
cost little, but avoid generating thousands of new strings per frame.
|
|
56
|
+
- **Images:** decoding is expensive and synchronous; reuse the same file (it is shared) and
|
|
57
|
+
use files at the displayed size.
|
|
58
|
+
- Heavy CPU work (parsing large files, computations) freezes the interface: split it into
|
|
59
|
+
chunks with `setTimeout(..., 0)` or use a `Worker`.
|