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
|
@@ -1,72 +1,299 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nodebuilder-ui
|
|
3
|
-
description:
|
|
3
|
+
description: nodebuilder widgets, style props and layout (Window, Column, Row, Scroll, Text, Button, TextField, Checkbox, Toggle, RadioGroup, Slider, Select, Tabs, Tooltip, Dialog, ProgressBar, Spinner, Card, For, Index, Show, Switch/Match, ErrorBoundary, theme), scrolling (vertical, horizontal, scrollbars, keyboard, scrollIntoView), context (createContext), ref (focus, scroll, measure), onResize, dark theme (setTheme), animations (createTween, createPresence), overlays (modal dialog, dropdown menu, hint/tooltip), opacity and translate, selectable text and clipboard (copy/paste). Use when creating or changing any screen, form, list, sidebar or layout of a nodebuilder app, when the user needs to copy text from the screen, or when an element does not show up, grow or scroll as expected.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# UI with nodebuilder
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
Everything comes from `@nodebuilderjs/core`. There is no DOM and no CSS: widgets are functions and
|
|
9
|
+
style is flat props. SolidJS-style reactivity: **only functions are reactive** (see AGENTS.md).
|
|
10
10
|
|
|
11
11
|
## Widgets
|
|
12
12
|
|
|
13
|
-
| Widget |
|
|
13
|
+
| Widget | What for | Main props |
|
|
14
14
|
|---|---|---|
|
|
15
|
-
| `Window` |
|
|
16
|
-
| `Column` / `Row` / `Box` |
|
|
17
|
-
| `Scroll` |
|
|
18
|
-
| `Text` |
|
|
19
|
-
| `Image` | PNG, JPEG, WebP, AVIF |
|
|
20
|
-
| `Icon` |
|
|
21
|
-
| `Canvas` |
|
|
22
|
-
| `Button` |
|
|
23
|
-
| `TextField` |
|
|
24
|
-
| `Checkbox` |
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
|
|
|
28
|
-
|
|
|
15
|
+
| `Window` | root, one per app, with its own title bar | `title`, `width`, `height` (initial size), `appId`, `onClose` (return `false` to keep it open) |
|
|
16
|
+
| `Column` / `Row` / `Box` | flex container (column, row; `Box` is a column) | style + events |
|
|
17
|
+
| `Scroll` | scrolling: vertical (default), `horizontal` or `horizontal vertical` (both) | needs `grow={1}` or a size; see "Scrolling" below |
|
|
18
|
+
| `Text` | text; wraps at spaces to fit the width | children (`{count}`, `"a"`) or `text={() => ...}`, `wrap` (default `true`), `maxLines` (truncates with "…"), `textAlign`, `lineHeight`, `selectable` |
|
|
19
|
+
| `Image` | PNG, JPEG, WebP, AVIF | see the `nodebuilder-images` skill |
|
|
20
|
+
| `Icon` | SVG icon (Lucide, Phosphor), color inherited like text | `svg`, `size` (20), `color`, `strokeWidth`; skill `nodebuilder-icons` |
|
|
21
|
+
| `Canvas` | free drawing: lines, polygons, arcs, text | `draw={(g) => ...}` + size; skill `nodebuilder-drawing` |
|
|
22
|
+
| `Button` | button with focus and keyboard | `label` or children, `variant` (`default`, `primary`, `danger`, `flat`), `disabled`, `onClick` |
|
|
23
|
+
| `TextField` | controlled text field | `value` (accessor), `onInput(value)`, `onSubmit`, `placeholder`, `disabled` |
|
|
24
|
+
| `Checkbox` | checkbox | `checked` (accessor), `onChange(checked)`, `label`, `disabled` |
|
|
25
|
+
| `Toggle` | on/off switch (`Switch` is the control-flow component) | `checked` (accessor), `onChange(on)`, `label`, `disabled` |
|
|
26
|
+
| `RadioGroup` | one option among several; a single Tab stop, arrows switch | `value` (accessor), `onChange(v)`, `options=[{ value, label, disabled? }]`, `disabled` |
|
|
27
|
+
| `Slider` | number by dragging or with arrows/PageUp/Home/End | `value` (accessor), `onChange(n)`, `min` (0), `max` (100), `step` (1), `width` (200), `disabled` |
|
|
28
|
+
| `Select` | dropdown list anchored to the field; full keyboard support | `value` (accessor, may be `undefined`), `onChange(v)`, `options=[{ value, label, disabled? }]`, `placeholder`, `width`, `disabled` |
|
|
29
|
+
| `Tabs` | tab bar (the content is yours, with `<Switch>`) | `value` (accessor), `onChange(v)`, `tabs=[{ value, label, disabled? }]` |
|
|
30
|
+
| `Tooltip` | hint when the pointer rests over the child | `text`, `placement` (`"top"`), `delay` (500 ms), child |
|
|
31
|
+
| `Dialog` | ready-made modal dialog: title, body, actions | `open` (accessor), `onClose`, `title`, `actions`, `width` (420), `dismissible` (true) |
|
|
32
|
+
| `ProgressBar` | progress bar, 6 px | `value` from 0 to 1 (without it: indeterminate) |
|
|
33
|
+
| `Spinner` | busy, no measure of progress | `size` (20), `color` |
|
|
34
|
+
| `Card` | column with background, border, radius 12, padding 16, gap 8 | style |
|
|
35
|
+
| `Divider` / `Spacer` | 1 px line / flexible space (optional fixed `size`) | |
|
|
36
|
+
| `<For each={list}>{(item, i) => ...}</For>` | list keyed by reference; `i` is an accessor | `fallback` |
|
|
37
|
+
| `<Index each={list}>{(item, i) => ...}</Index>` | list by position (primitives); `item` is an accessor, `i` a number | `fallback` |
|
|
38
|
+
| `<Show when={() => ...} fallback={...}>` | conditional; a `{(v) => ...}` child gets the value as an accessor | |
|
|
39
|
+
| `<Switch fallback={...}><Match when={...}>...</Match></Switch>` | several cases: the first true `Match` | `Match` accepts `{(v) => ...}` |
|
|
40
|
+
| `<ErrorBoundary fallback={(error, reset) => ...}>` | replaces children that threw with the `fallback` | `reset()` tries again |
|
|
29
41
|
|
|
30
42
|
```tsx
|
|
31
|
-
const [
|
|
32
|
-
<TextField value={
|
|
33
|
-
<Checkbox checked={
|
|
34
|
-
<Button label="
|
|
43
|
+
const [name, setName] = createSignal("");
|
|
44
|
+
<TextField value={name} onInput={setName} onSubmit={save} placeholder="Name" />
|
|
45
|
+
<Checkbox checked={active} onChange={setActive} label="Active" />
|
|
46
|
+
<Button label="Save" variant="primary" disabled={() => !name()} onClick={save} />
|
|
47
|
+
<Toggle checked={wifi} onChange={setWifi} label="Wi-Fi" />
|
|
48
|
+
<Slider value={volume} onChange={setVolume} max={100} step={5} />
|
|
35
49
|
```
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
`disabled` (value or function) on any control: dims it, removes it from Tab order and focus, and
|
|
52
|
+
ignores clicks and keys. Do not fake it with `opacity` + an empty `onClick`.
|
|
38
53
|
|
|
39
|
-
|
|
54
|
+
## Style props (any widget)
|
|
55
|
+
|
|
56
|
+
- Layout: `direction` (`"row"`/`"column"`), `gap`, `padding` (`8`, `[v, h]` or
|
|
40
57
|
`[top, right, bottom, left]`), `width`, `height`, `minWidth`, `minHeight`, `maxWidth`,
|
|
41
58
|
`maxHeight`, `grow`, `align`, `justify`, `position: "absolute"` + `left`/`top`, `clip`.
|
|
42
|
-
- Visual: `background`, `radius`, `borderWidth`, `borderColor`, `shadow={{ color, blur, y }}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`onPointerMove`
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
- Visual: `background`, `radius`, `borderWidth`, `borderColor`, `shadow={{ color, blur, y }}`,
|
|
60
|
+
`opacity` (0 to 1, applies to children), `translateX`/`translateY` (shifts after
|
|
61
|
+
layout, without pushing siblings; for animation).
|
|
62
|
+
- Text (inherited by children): `color`, `fontSize`, `bold`. Cursor: `cursor="pointer"`.
|
|
63
|
+
- Colors are hex strings (`"#3584e4"`, `"#00000010"` with alpha) or `"transparent"`.
|
|
64
|
+
- Events on any container: `onClick`, `onPointerDown/Up/Enter/Leave`,
|
|
65
|
+
`onPointerMove`, `onScroll`, `onKeyDown`, `onFocus`, `onBlur`. Pointer events carry
|
|
66
|
+
`x`/`y` (window) and `localX`/`localY` (relative to the node); while the button is held,
|
|
67
|
+
`onPointerMove` keeps reaching the node where the press started, even outside it (dragging,
|
|
68
|
+
sliders). `onKeyDown` only arrives along the path of the focused element.
|
|
69
|
+
- Any prop accepts a fixed value or a function: `background={() => (sel() ? a : b)}`.
|
|
70
|
+
- `onResize={(s) => ...}`: the node's size (`s.width`, `s.height`) after layout, the first
|
|
71
|
+
time and whenever it changes. Use it to adapt the screen to the available width.
|
|
72
|
+
- `ref={(el) => ...}` on any widget: `el.focus()`, `el.blur()`, `el.scrollIntoView()`,
|
|
73
|
+
on a `Scroll` `el.scrollTo(y)` / `el.scrollTo({ x, y })` and `el.scrollX`/`el.scrollY`, and
|
|
74
|
+
`el.x`/`y`/`width`/`height` from the last layout. It is called on creation, before layout:
|
|
75
|
+
keep the `el` and read the box later (in a click, for example).
|
|
76
|
+
|
|
77
|
+
## Conditions, lists and errors
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { Button, ErrorBoundary, Index, Match, Show, Switch, Text, createSignal } from "@nodebuilderjs/core";
|
|
81
|
+
|
|
82
|
+
const [order, setOrder] = createSignal<{ id: number } | null>(null);
|
|
83
|
+
const [step, setStep] = createSignal<"cart" | "payment" | "done">("cart");
|
|
84
|
+
const [grades] = createSignal([7, 9, 10]);
|
|
85
|
+
|
|
86
|
+
<Show when={order} fallback={<Text>No order</Text>}>
|
|
87
|
+
{(o) => <Text>{() => `Order ${o().id}`}</Text>}
|
|
88
|
+
</Show>;
|
|
89
|
+
|
|
90
|
+
<Switch fallback={<Text>Thank you!</Text>}>
|
|
91
|
+
<Match when={() => step() === "cart"}><Text>Your cart</Text></Match>
|
|
92
|
+
<Match when={() => step() === "payment"}><Text>Payment</Text></Match>
|
|
93
|
+
</Switch>;
|
|
94
|
+
|
|
95
|
+
<Index each={grades}>{(grade, i) => <Text>{() => `Test ${i + 1}: ${grade()}`}</Text>}</Index>;
|
|
96
|
+
|
|
97
|
+
<ErrorBoundary fallback={(error, reset) => <Button label="Try again" onClick={reset} />}>
|
|
98
|
+
<Text>{() => `Order ${order()!.id}`}</Text>
|
|
99
|
+
</ErrorBoundary>;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `<For>` vs `<Index>`: `For` tracks each **object** (the row survives reordering; recreating
|
|
103
|
+
the objects recreates the rows). `Index` tracks each **position**: the row stays and the
|
|
104
|
+
`item()` accessor changes; good for lists of numbers/strings or data that always arrives as new
|
|
105
|
+
objects.
|
|
106
|
+
- A function child of `Show`/`Match` gets the value only when it takes a parameter: `{(o) => ...}`
|
|
107
|
+
gets the value as an accessor; `{() => ...}` (no parameter) is a plain reactive child.
|
|
108
|
+
- `Switch` only accepts `Match` children, written in JSX (not generated by a function).
|
|
109
|
+
- `ErrorBoundary` catches errors while creating the children and later in their effects (e.g. a click
|
|
110
|
+
changes a signal and a text throws). It disposes everything the children created and shows the
|
|
111
|
+
`fallback`; `reset()` recreates the children. What the `onClick` itself throws is not caught:
|
|
112
|
+
handle it with `try/catch` in the handler.
|
|
113
|
+
|
|
114
|
+
## Context, ref and size
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { Column, Scroll, TextField, Text, createContext, createSignal, useContext } from "@nodebuilderjs/core";
|
|
118
|
+
import type { Accessor, ElementRef } from "@nodebuilderjs/core";
|
|
119
|
+
|
|
120
|
+
// Value shared through the tree (theme, language, logged-in user), without passing props.
|
|
121
|
+
const User = createContext<Accessor<string>>(() => "anonymous");
|
|
122
|
+
function Greeting() {
|
|
123
|
+
const user = useContext(User);
|
|
124
|
+
return <Text>{() => `Hello, ${user()}`}</Text>;
|
|
125
|
+
}
|
|
126
|
+
const [user, setUser] = createSignal("Ana");
|
|
127
|
+
<User.Provider value={user}><Greeting /></User.Provider>;
|
|
128
|
+
|
|
129
|
+
// Focus a field on open and scroll a list to the end.
|
|
130
|
+
const [search, setSearch] = createSignal("");
|
|
131
|
+
let list: ElementRef | undefined;
|
|
132
|
+
<Column grow={1}>
|
|
133
|
+
<TextField ref={(el) => el.focus()} value={search} onInput={setSearch} />
|
|
134
|
+
<Scroll ref={(el) => (list = el)} grow={1}>{/* ... */}</Scroll>
|
|
135
|
+
</Column>;
|
|
136
|
+
const scrollToEnd = () => list?.scrollTo(Infinity);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- The context value is read once, when the component runs. For a value that changes, pass
|
|
140
|
+
an accessor (signal) and call it inside a reactive function, as above.
|
|
141
|
+
- `useContext` only works in the component body (not inside an `onClick` or `setTimeout`):
|
|
142
|
+
read it there and keep it in a variable.
|
|
143
|
+
|
|
144
|
+
## Scrolling
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { Button, Card, For, Row, Scroll, Text } from "@nodebuilderjs/core";
|
|
148
|
+
import type { ElementRef } from "@nodebuilderjs/core";
|
|
149
|
+
|
|
150
|
+
const items = Array.from({ length: 200 }, (_, i) => `Item ${i}`);
|
|
151
|
+
const rows: ElementRef[] = [];
|
|
152
|
+
<>
|
|
153
|
+
{/* Strip that scrolls sideways: children keep their width. */}
|
|
154
|
+
<Scroll horizontal gap={12} padding={[0, 0, 14, 0]}>
|
|
155
|
+
<For each={items}>{(t) => <Card width={160}><Text>{t}</Text></Card>}</For>
|
|
156
|
+
</Scroll>
|
|
157
|
+
<Button label="Go to 150" onClick={() => rows[150]?.scrollIntoView()} />
|
|
158
|
+
<Scroll grow={1}>
|
|
159
|
+
<For each={items}>{(t, i) => <Row ref={(el) => (rows[i()] = el)} padding={8}><Text>{t}</Text></Row>}</For>
|
|
160
|
+
</Scroll>
|
|
161
|
+
{/* Wide and long table: scrolls on both axes. */}
|
|
162
|
+
<Scroll horizontal vertical height={300}>{/* rows with fixed widths */}</Scroll>
|
|
163
|
+
</>;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- `<Scroll>` scrolls vertically. `horizontal`: a row whose children keep their width (text
|
|
167
|
+
inside does not wrap, unless given `width`/`maxWidth`). `horizontal vertical`: a column
|
|
168
|
+
that scrolls on both axes; each child is at least the visible width and grows if the
|
|
169
|
+
content asks for it.
|
|
170
|
+
- The user scrolls with the wheel (Shift+wheel goes sideways; touchpad on both axes), by dragging
|
|
171
|
+
the scrollbar thumb, by clicking the track (one page) and with the keyboard: PageUp/PageDown,
|
|
172
|
+
Home/End and the arrows scroll the `Scroll` around the focused element (with no focus, the one under
|
|
173
|
+
the mouse), unless an `onKeyDown` calls `e.stop()`. `TextField` keeps Home/End and ←/→;
|
|
174
|
+
↑/↓ scroll.
|
|
175
|
+
- Scrollbars sit **on top of** the content, in a 14 px strip along the right edge (and the
|
|
176
|
+
bottom one), and only exist when there is something to scroll: leave `padding` on that edge if
|
|
177
|
+
buttons touch it (like the `padding={[0, 0, 14, 0]}` above).
|
|
178
|
+
- `el.scrollIntoView()` (on any element, via `ref`) scrolls the surrounding `Scroll`s the minimum
|
|
179
|
+
needed for it to show; it takes effect on the next frame, even for a just-created or off-screen
|
|
180
|
+
element. `el.scrollTo({ x, y })` accepts just one of the axes.
|
|
181
|
+
- An open `<Overlay modal>` blocks the wheel, keys and scrollbars of what is behind it.
|
|
182
|
+
|
|
183
|
+
## Dialogs, menus and hints: `<Overlay>`
|
|
184
|
+
|
|
185
|
+
For the common cases use the ready-made widgets (`Dialog`, `Select`, `Tooltip`), which already handle
|
|
186
|
+
focus, keyboard and closing. `<Overlay>` is for what they do not cover (options menu, toast,
|
|
187
|
+
custom popover):
|
|
188
|
+
|
|
189
|
+
No `zIndex` and no `position: fixed`: whatever must stay on top of everything (outside the
|
|
190
|
+
clipping of a `Scroll` or `Card`) goes in an `<Overlay>`, almost always inside a `<Show>`.
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
import { Button, Card, Overlay, Row, Show, Text, createSignal, theme } from "@nodebuilderjs/core";
|
|
194
|
+
import type { ElementRef } from "@nodebuilderjs/core";
|
|
195
|
+
|
|
196
|
+
const [menu, setMenu] = createSignal(false);
|
|
197
|
+
const [confirm, setConfirm] = createSignal(false);
|
|
198
|
+
let button: ElementRef | undefined;
|
|
199
|
+
<>
|
|
200
|
+
<Button ref={(el) => (button = el)} label="Options" onClick={() => setMenu((m) => !m)} />
|
|
201
|
+
<Show when={menu}>
|
|
202
|
+
<Overlay anchor={() => button} matchWidth onDismiss={() => setMenu(false)}>
|
|
203
|
+
<Card padding={4} shadow={{ color: "#00000030", blur: 16, y: 4 }}>
|
|
204
|
+
<Button label="Delete" variant="flat" onClick={() => (setMenu(false), setConfirm(true))} />
|
|
205
|
+
</Card>
|
|
206
|
+
</Overlay>
|
|
207
|
+
</Show>
|
|
208
|
+
<Show when={confirm}>
|
|
209
|
+
<Overlay modal background="#00000059" align="center" justify="center" onDismiss={() => setConfirm(false)}>
|
|
210
|
+
<Card width={320} gap={12}>
|
|
211
|
+
<Text bold>Delete file?</Text>
|
|
212
|
+
<Row gap={8} justify="end">
|
|
213
|
+
<Button label="Cancel" variant="flat" onClick={() => setConfirm(false)} />
|
|
214
|
+
<Button label="Delete" variant="danger" onClick={() => setConfirm(false)} />
|
|
215
|
+
</Row>
|
|
216
|
+
</Card>
|
|
217
|
+
</Overlay>
|
|
218
|
+
</Show>
|
|
219
|
+
</>;
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
- `anchor={() => el}` (always as a function): sits next to the element, `placement`
|
|
223
|
+
`"bottom"` (default), `"top"`, `"right"` or `"left"`, flips side if it does not fit, never leaves the
|
|
224
|
+
window; `offset` (4) and `matchWidth` (same width, for menus).
|
|
225
|
+
- Without `anchor`: covers the window; position it with `align`/`justify`/`padding` or with
|
|
226
|
+
`position="absolute"` + `left`/`top`. Empty space lets clicks through (toasts, hints).
|
|
227
|
+
- `modal`: blocks clicks and Tab on what is behind; `background` dims the backdrop.
|
|
228
|
+
- `onDismiss`: click outside (when modal, on the dimmed backdrop; never on the anchor) or Esc. Close the
|
|
229
|
+
`<Show>` there.
|
|
230
|
+
- Declare the `<Overlay>` **outside** a `Button` (next to it, as above): events inside
|
|
231
|
+
the overlay bubble up through the parents of the place where it was declared.
|
|
232
|
+
- Transition: animate `opacity` and `translateY` with a signal updated in
|
|
233
|
+
`requestAnimationFrame` (see `examples/overlays.tsx` in the nodebuilder repository).
|
|
234
|
+
|
|
235
|
+
## Layout rules (simplified flexbox)
|
|
236
|
+
|
|
237
|
+
- Default `align` is **stretch**: children take the full width of a column. Use
|
|
238
|
+
`align="start"`/`"center"` to shrink them to their content.
|
|
239
|
+
- `justify`: `start`, `center`, `end`, `space-between`. There is no `space-around`/`evenly`.
|
|
240
|
+
- `grow={1}` splits the leftover space. Without `grow`, the size is the content's.
|
|
241
|
+
- A child **does not shrink below its own content** (like CSS `min-size: auto`), except
|
|
242
|
+
`Scroll`, `clip` or an explicit `minWidth/minHeight`. A long list inside a column
|
|
243
|
+
that keeps growing? Wrap it in `<Scroll grow={1}>`.
|
|
244
|
+
- The content of a `Scroll` does not count toward that minimum: the column (or row) around it
|
|
245
|
+
shrinks without needing `minHeight={0}`/`minWidth={0}`. A `Scroll horizontal` does not
|
|
246
|
+
lose height (only what scrolls vertically gives up height).
|
|
247
|
+
- In a `Row` without room, whoever has `grow` shrinks first: the long text gets
|
|
248
|
+
`grow={1}` (and `maxLines={1}` to become "…"), the short labels next to it stay whole.
|
|
249
|
+
- Text wraps by itself at the width it gets (`\n` also breaks); a word longer than the line
|
|
250
|
+
overflows. A single line: `wrap={false}` (overflows) or `maxLines={1}` (truncates with "…").
|
|
251
|
+
- There are no percentages, `flex-wrap` or grid: build rows with `Row` and fixed widths (see
|
|
252
|
+
tables: `Text width={110}`) or `grow`.
|
|
253
|
+
|
|
254
|
+
## Theme and window
|
|
255
|
+
|
|
256
|
+
- Default colors and sizes in `theme` (`theme.text.secondary`, `theme.accent.base`,
|
|
257
|
+
`theme.danger.base`, `theme.divider`...). Dark theme: `setTheme("dark")` (or `"light"`, or
|
|
258
|
+
`{ ...darkTheme, accent: {...} }`), at any time; `colorScheme()` tells which one is active.
|
|
259
|
+
- **Theme colors always inside a function** (`color={() => theme.text.secondary}`): that way they
|
|
260
|
+
follow theme changes. `color={theme.text.secondary}` keeps the color of that moment.
|
|
69
261
|
- `windowControls.close()`, `.minimize()`, `.toggleMaximize()`, `.setTitle(t)`;
|
|
70
|
-
`windowState.width()`, `.height()`, `.maximized()`, `.activated()`
|
|
71
|
-
-
|
|
72
|
-
|
|
262
|
+
`windowState.width()`, `.height()`, `.maximized()`, `.activated()` are accessors.
|
|
263
|
+
- Animation: `createTween(() => target)` (a number that moves smoothly), `createPresence(when)`
|
|
264
|
+
(`mounted` for `<Show>`, `progress` 0→1 for `opacity`/`translateY`), `animate(...)`.
|
|
265
|
+
`Dialog`, `Select`, `Tooltip` and `Toggle` already animate on their own.
|
|
266
|
+
- Not available yet: theme following the system, `scale`/`rotate`, selection in `TextField`
|
|
267
|
+
(and Ctrl+C in it), IME, multiple windows.
|
|
268
|
+
|
|
269
|
+
## Selecting and copying text
|
|
270
|
+
|
|
271
|
+
Plain text **does not select**, on purpose (labels, buttons, titles). Text the user
|
|
272
|
+
may want to copy (ids, links, paths, error messages, results) gets `selectable`:
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
<Text selectable>{order.id}</Text>
|
|
276
|
+
<Text selectable color={theme.danger.base}>{error}</Text>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
With `selectable`: dragging selects, double-click takes the word, triple-click the line,
|
|
280
|
+
Ctrl+A everything and Ctrl+C copies; the cursor becomes the text cursor. It is not in the Tab order, and inside
|
|
281
|
+
the title bar it selects instead of dragging the window. Selection is one `Text` at a time
|
|
282
|
+
(it does not span several) and there is no Shift+click or Shift+arrows yet.
|
|
283
|
+
|
|
284
|
+
The system clipboard (text only) is in `clipboard`:
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
import { Button, clipboard, createSignal } from "@nodebuilderjs/core";
|
|
288
|
+
|
|
289
|
+
const [link, setLink] = createSignal("https://example.com");
|
|
290
|
+
<Button label="Copy link" onClick={() => clipboard.writeText(link())} />
|
|
291
|
+
<Button label="Paste" onClick={async () => setLink(await clipboard.readText())} />
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
- `readText()` is asynchronous (another program holds the content) and returns `""` if there
|
|
295
|
+
is no text.
|
|
296
|
+
- Copy from a click or key press (as above): on Wayland the system ignores copies that
|
|
297
|
+
do not come right after a user interaction (a `setTimeout` will not do).
|
|
298
|
+
- `TextField` already pastes with Ctrl+V / Shift+Insert (line breaks become spaces), but
|
|
299
|
+
does not select or copy yet.
|
package/template/AGENTS.md
CHANGED
|
@@ -1,34 +1,37 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
nodebuilder app: native Linux UI in TSX, **no DOM and no React**. Read this before changing code.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
- `bun run dev`
|
|
7
|
-
-
|
|
5
|
+
## Commands
|
|
6
|
+
- `bun run dev` opens the app; `bun test` runs the tests without a window; `bun run typecheck` checks types.
|
|
7
|
+
- Before finishing: `bun run typecheck && bun test`.
|
|
8
8
|
|
|
9
|
-
##
|
|
10
|
-
- **
|
|
11
|
-
|
|
12
|
-
- **
|
|
13
|
-
`<Text>{count()}</Text>`
|
|
14
|
-
`background={() => (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
## Model (like SolidJS, not React)
|
|
10
|
+
- **Components run once.** There is no re-render and there are no hooks. State is `createSignal`;
|
|
11
|
+
derived values are `createMemo`; effects are `createEffect` (+ `onCleanup`).
|
|
12
|
+
- **Only functions are reactive.** `<Text>{count}</Text>` or `text={() => ...}` update;
|
|
13
|
+
`<Text>{count()}</Text>` is read once and never changes. The same goes for styles:
|
|
14
|
+
`background={() => (active() ? "#3584e4" : "#0000")}`, and for theme colors:
|
|
15
|
+
`color={() => theme.text.secondary}` follows a switch to the dark theme.
|
|
16
|
+
- Lists: `<For each={list}>{(item) => ...}</For>` (keyed by reference) or
|
|
17
|
+
`<Index each={list}>{(item) => ...}</Index>` (by position; `item` is an accessor).
|
|
18
|
+
Conditions: `<Show when={() => ...} fallback={...}>`; several cases: `<Switch>` with
|
|
19
|
+
`<Match when={...}>`. Errors: `<ErrorBoundary fallback={(error, reset) => ...}>`.
|
|
20
|
+
- Styles are flat props (`padding`, `gap`, `grow`, `background`...), not CSS. There is no
|
|
21
|
+
`className`, `style={{}}`, `div` or `useState`, and React libraries (e.g. `lucide-react`) don't work.
|
|
19
22
|
|
|
20
|
-
## Skills (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
## Skills (details by topic)
|
|
24
|
+
In `.agents/skills/<name>/SKILL.md` (linked from `.claude/skills/` for Claude Code). Agents
|
|
25
|
+
that support skills load them on their own; others should read the topic's skill before starting:
|
|
23
26
|
|
|
24
|
-
| Skill |
|
|
27
|
+
| Skill | When |
|
|
25
28
|
|---|---|
|
|
26
|
-
| `nodebuilder-ui` |
|
|
27
|
-
| `nodebuilder-
|
|
28
|
-
| `nodebuilder-
|
|
29
|
-
| `nodebuilder-
|
|
30
|
-
| `nodebuilder-
|
|
31
|
-
| `nodebuilder-
|
|
32
|
-
| `nodebuilder-
|
|
33
|
-
| `nodebuilder-
|
|
34
|
-
| `nodebuilder-
|
|
29
|
+
| `nodebuilder-ui` | screens, widgets (incl. `Toggle`, `RadioGroup`, `Slider`, `Select`, `Tabs`, `Tooltip`, `Dialog`, `ProgressBar`, `disabled`), style props, layout, `For`/`Index`/`Show`/`Switch`, `ErrorBoundary`, scrolling (horizontal, scrollbars, keyboard, `scrollIntoView`), context, `ref` (focus, scroll), `onResize`, `Overlay` (dialog, menu, tooltip), `opacity`/`translate`, theme (light/dark, `setTheme`), animations, selectable text, copy/paste |
|
|
30
|
+
| `nodebuilder-from-react` | before installing libraries or porting React/web code |
|
|
31
|
+
| `nodebuilder-images` | images (PNG, JPEG, WebP, AVIF), files in the app |
|
|
32
|
+
| `nodebuilder-icons` | icons (Lucide, Phosphor) and SVG with `Icon` |
|
|
33
|
+
| `nodebuilder-charts` | ready-made charts (`@nodebuilderjs/core/charts`, Recharts API) |
|
|
34
|
+
| `nodebuilder-drawing` | freehand drawing with `Canvas` |
|
|
35
|
+
| `nodebuilder-testing` | tests without a window (`mount`, `findByText`, `click`, `interact`, `frames`, clipboard) |
|
|
36
|
+
| `nodebuilder-packaging` | AppImage/.deb/.rpm, embedded files, icon and app id |
|
|
37
|
+
| `nodebuilder-performance` | slowness, long lists, live data, animation |
|
package/template/README.md
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
# {{title}}
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Native Linux desktop app made with [nodebuilder](https://www.npmjs.com/package/@nodebuilderjs/core):
|
|
4
|
+
TSX components with SolidJS-style reactivity, drawn by a native core (Wayland/X11 + GLES).
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
|
-
bun run dev #
|
|
8
|
-
bun start #
|
|
9
|
-
bun test #
|
|
7
|
+
bun run dev # opens the window and restarts on save
|
|
8
|
+
bun start # runs once
|
|
9
|
+
bun test # tests without a window (test/)
|
|
10
10
|
bun run typecheck # TypeScript 7
|
|
11
|
-
bun run package # AppImage, .deb, .rpm
|
|
11
|
+
bun run package # AppImage, .deb, .rpm and .tar.gz in dist/
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Structure
|
|
15
15
|
|
|
16
16
|
```
|
|
17
17
|
src/
|
|
18
|
-
main.tsx
|
|
19
|
-
App.tsx layout
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
main.tsx opens the window: run(() => <App />)
|
|
19
|
+
App.tsx screen layout and dark mode toggle; creates the state and passes it down
|
|
20
|
+
state.ts app signals and actions (counter, tasks, history)
|
|
21
|
+
icons.ts SVG icons used by the app
|
|
22
|
+
components/
|
|
23
|
+
Counter.tsx buttons, reactive text and a tooltip
|
|
24
|
+
Tasks.tsx text field, list with For/Checkbox, Show and a confirmation Dialog
|
|
25
|
+
Activity.tsx chart (@nodebuilderjs/core/charts) that follows the state
|
|
26
26
|
test/
|
|
27
|
-
App.test.tsx
|
|
27
|
+
App.test.tsx tests without a window: mount + interact (click, type)
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
To remove the example and start from scratch, keep only the `<Window>` in `App.tsx`.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Requirements: Linux with Wayland or X11, [Bun](https://bun.sh) 1.4+.
|
package/template/src/App.tsx
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
import { Column, Icon, Row, Scroll, Text, Window, theme } from "@nodebuilderjs/core";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { Column, Icon, Row, Scroll, Spacer, Text, Toggle, Window, colorScheme, setTheme, theme } from "@nodebuilderjs/core";
|
|
2
|
+
import { Activity } from "./components/Activity.js";
|
|
3
|
+
import { Counter } from "./components/Counter.js";
|
|
4
|
+
import { Tasks } from "./components/Tasks.js";
|
|
5
|
+
import { sparkle } from "./icons.js";
|
|
6
|
+
import { createState } from "./state.js";
|
|
7
7
|
|
|
8
8
|
export function App() {
|
|
9
|
-
const
|
|
9
|
+
const state = createState();
|
|
10
10
|
return (
|
|
11
11
|
<Window title="{{title}}" appId="{{appId}}" width={720} height={760}>
|
|
12
12
|
<Scroll grow={1} padding={20} gap={16}>
|
|
13
13
|
<Row gap={12} align="center">
|
|
14
|
-
|
|
15
|
-
<
|
|
14
|
+
{/* Theme colors go inside functions, so they follow a theme switch. */}
|
|
15
|
+
<Icon svg={sparkle} size={32} color={() => theme.accent.base} />
|
|
16
|
+
<Column gap={2} grow={1}>
|
|
16
17
|
<Text fontSize={22} bold>{"{{title}}"}</Text>
|
|
17
|
-
<Text color={theme.text.secondary}>
|
|
18
|
+
<Text color={() => theme.text.secondary}>Edit src/App.tsx and save: bun run dev reopens the window.</Text>
|
|
18
19
|
</Column>
|
|
20
|
+
<Spacer size={8} />
|
|
21
|
+
<Toggle checked={() => colorScheme() === "dark"} onChange={(on) => setTheme(on ? "dark" : "light")} label="Dark" />
|
|
19
22
|
</Row>
|
|
20
23
|
<Row gap={16} align="start">
|
|
21
24
|
<Column grow={1} gap={16}>
|
|
22
|
-
<
|
|
23
|
-
<
|
|
25
|
+
<Counter state={state} />
|
|
26
|
+
<Activity state={state} />
|
|
24
27
|
</Column>
|
|
25
28
|
<Column grow={1}>
|
|
26
|
-
<
|
|
29
|
+
<Tasks state={state} />
|
|
27
30
|
</Column>
|
|
28
31
|
</Row>
|
|
29
32
|
</Scroll>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Card, Icon, Row, Text, theme } from "@nodebuilderjs/core";
|
|
2
|
+
import { Area, AreaChart, CartesianGrid, Legend, Line, Tooltip, YAxis } from "@nodebuilderjs/core/charts";
|
|
3
|
+
import { chart } from "../icons.js";
|
|
4
|
+
import type { State } from "../state.js";
|
|
5
|
+
|
|
6
|
+
export function Activity(props: { state: State }) {
|
|
7
|
+
return (
|
|
8
|
+
<Card gap={8}>
|
|
9
|
+
<Row gap={8} align="center" color={() => theme.text.secondary}>
|
|
10
|
+
<Icon svg={chart} size={16} />
|
|
11
|
+
<Text bold>Activity</Text>
|
|
12
|
+
</Row>
|
|
13
|
+
{/* data is a signal: the chart follows every click and every task. */}
|
|
14
|
+
<AreaChart data={props.state.history} height={180}>
|
|
15
|
+
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
|
16
|
+
<YAxis />
|
|
17
|
+
<Area dataKey="clicks" name="Clicks" type="monotone" />
|
|
18
|
+
<Line dataKey="pending" name="Pending" stroke="#e5a50a" dot={false} />
|
|
19
|
+
<Tooltip />
|
|
20
|
+
<Legend />
|
|
21
|
+
</AreaChart>
|
|
22
|
+
</Card>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Button, Card, Icon, Row, Text, Tooltip, theme } from "@nodebuilderjs/core";
|
|
2
|
+
import { pointer } from "../icons.js";
|
|
3
|
+
import type { State } from "../state.js";
|
|
4
|
+
|
|
5
|
+
export function Counter(props: { state: State }) {
|
|
6
|
+
const { clicks, click, reset } = props.state;
|
|
7
|
+
// Only functions are reactive: Text reads `times` again whenever `clicks` changes.
|
|
8
|
+
const times = () => `You clicked ${clicks()} ${clicks() === 1 ? "time" : "times"}`;
|
|
9
|
+
return (
|
|
10
|
+
<Card gap={12}>
|
|
11
|
+
<Row gap={8} align="center" color={() => theme.text.secondary}>
|
|
12
|
+
<Icon svg={pointer} size={16} />
|
|
13
|
+
<Text bold>Counter</Text>
|
|
14
|
+
</Row>
|
|
15
|
+
<Text fontSize={20}>{times}</Text>
|
|
16
|
+
<Row gap={8}>
|
|
17
|
+
<Tooltip text="Back to zero">
|
|
18
|
+
<Button label="Reset" variant="flat" onClick={reset} disabled={() => clicks() === 0} />
|
|
19
|
+
</Tooltip>
|
|
20
|
+
<Button label="Click me" variant="primary" onClick={click} />
|
|
21
|
+
</Row>
|
|
22
|
+
</Card>
|
|
23
|
+
);
|
|
24
|
+
}
|