create-nodebuilderjs 0.2.0 → 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.
Files changed (28) hide show
  1. package/index.js +10 -10
  2. package/package.json +2 -2
  3. package/template/.agents/skills/nodebuilder-charts/SKILL.md +87 -0
  4. package/template/.agents/skills/nodebuilder-drawing/SKILL.md +54 -0
  5. package/template/.agents/skills/nodebuilder-from-react/SKILL.md +69 -0
  6. package/template/.agents/skills/nodebuilder-icons/SKILL.md +53 -0
  7. package/template/.agents/skills/nodebuilder-images/SKILL.md +50 -0
  8. package/template/.agents/skills/nodebuilder-packaging/SKILL.md +37 -0
  9. package/template/.agents/skills/nodebuilder-performance/SKILL.md +40 -0
  10. package/template/.agents/skills/nodebuilder-testing/SKILL.md +97 -0
  11. package/template/.agents/skills/nodebuilder-ui/SKILL.md +282 -55
  12. package/template/AGENTS.md +30 -27
  13. package/template/README.md +25 -7
  14. package/template/src/App.tsx +28 -17
  15. package/template/src/components/Activity.tsx +24 -0
  16. package/template/src/components/Counter.tsx +24 -0
  17. package/template/src/components/Tasks.tsx +54 -0
  18. package/template/src/icons.ts +11 -0
  19. package/template/src/state.ts +50 -0
  20. package/template/test/App.test.tsx +54 -9
  21. package/template/.agents/skills/nodebuilder-desempenho/SKILL.md +0 -36
  22. package/template/.agents/skills/nodebuilder-desenho/SKILL.md +0 -54
  23. package/template/.agents/skills/nodebuilder-empacotar/SKILL.md +0 -37
  24. package/template/.agents/skills/nodebuilder-graficos/SKILL.md +0 -87
  25. package/template/.agents/skills/nodebuilder-icones/SKILL.md +0 -53
  26. package/template/.agents/skills/nodebuilder-imagens/SKILL.md +0 -50
  27. package/template/.agents/skills/nodebuilder-sem-react/SKILL.md +0 -48
  28. package/template/.agents/skills/nodebuilder-testes/SKILL.md +0 -56
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // bun create nodebuilderjs [dir] [--title "Meu App"] [--no-install] [--no-git]
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(`uso: bun create nodebuilderjs [pasta] [--title "Meu App"] [--no-install] [--no-git]`);
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("Nome do projeto: (meu-app) ")).trim() || "meu-app";
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(`A pasta ${dir} já existe e não está vazia.`);
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 não encontrado: instale em https://bun.sh e rode `bun install`.");
88
- else if (!run("bun", ["install"])) console.warn("`bun install` falhou; rode de novo dentro da pasta.");
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
- Pronto! ${title} criado em ${cd}
96
+ Done! ${title} created in ${cd}
97
97
 
98
98
  cd ${cd}
99
- bun run dev # abre a janela e recarrega ao salvar
100
- bun test # testes sem janela
101
- bun run package # AppImage, .deb, .rpm e .tar.gz em dist/
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.0",
4
- "description": "Cria um app nodebuilder: bun create nodebuilderjs meu-app",
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`.