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.
Files changed (33) 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 +18 -18
  14. package/template/src/App.tsx +16 -13
  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 +45 -35
  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
  29. package/template/src/componentes/Atividade.tsx +0 -24
  30. package/template/src/componentes/Contador.tsx +0 -22
  31. package/template/src/componentes/Tarefas.tsx +0 -38
  32. package/template/src/estado.ts +0 -50
  33. package/template/src/icones.ts +0 -11
@@ -1,72 +1,299 @@
1
1
  ---
2
2
  name: nodebuilder-ui
3
- description: Widgets, props de estilo e layout do nodebuilder (Window, Column, Row, Scroll, Text, Button, TextField, Checkbox, Card, For, Show, theme). Use ao criar ou mudar qualquer tela, formulário, lista, barra lateral ou layout de um app nodebuilder, ou quando um elemento não aparece, não cresce ou não rola como esperado.
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
- # Interface com nodebuilder
6
+ # UI with nodebuilder
7
7
 
8
- Tudo vem de `@nodebuilderjs/core`. Não há DOM nem CSS: widgets são funções e o estilo são
9
- props planas. Reatividade estilo SolidJS: **só funções são reativas** (veja o AGENTS.md).
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 | Para quê | Props principais |
13
+ | Widget | What for | Main props |
14
14
  |---|---|---|
15
- | `Window` | raiz, uma por app, com barra de título própria | `title`, `width`, `height` (tamanho inicial), `appId`, `onClose` (retorne `false` para não fechar) |
16
- | `Column` / `Row` / `Box` | contêiner flex (coluna, linha; `Box` é coluna) | estilo + eventos |
17
- | `Scroll` | coluna com rolagem vertical | precisa de `grow={1}` ou `height`; filhos podem ser maiores |
18
- | `Text` | texto de uma linha (sem quebra automática) | filhos (`{count}`, `"a"`) ou `text={() => ...}` |
19
- | `Image` | PNG, JPEG, WebP, AVIF | veja a skill `nodebuilder-imagens` |
20
- | `Icon` | ícone SVG (Lucide, Phosphor), cor herdada como texto | `svg`, `size` (20), `color`, `strokeWidth`; skill `nodebuilder-icones` |
21
- | `Canvas` | desenho livre: linhas, polígonos, arcos, texto | `draw={(g) => ...}` + tamanho; skill `nodebuilder-desenho` |
22
- | `Button` | botão com foco e teclado | `label` ou filhos, `variant` (`default`, `primary`, `danger`, `flat`), `disabled`, `onClick` |
23
- | `TextField` | campo de texto controlado | `value` (accessor), `onInput(valor)`, `onSubmit`, `placeholder` |
24
- | `Checkbox` | caixa de seleção | `checked` (accessor), `onChange(marcado)`, `label` |
25
- | `Card` | coluna com fundo, borda, raio 12, padding 16, gap 8 | estilo |
26
- | `Divider` / `Spacer` | linha de 1 px / espaço flexível (`size` fixo opcional) | |
27
- | `<For each={lista}>{(item, i) => ...}</For>` | lista keyed por referência; `i` é um accessor | `fallback` |
28
- | `<Show when={() => ...} fallback={...}>` | condicional | |
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 [nome, setNome] = createSignal("");
32
- <TextField value={nome} onInput={setNome} onSubmit={salvar} placeholder="Nome" />
33
- <Checkbox checked={ativo} onChange={setAtivo} label="Ativo" />
34
- <Button label="Salvar" variant="primary" disabled={() => !nome()} onClick={salvar} />
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
- ## Props de estilo (qualquer widget)
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
- - Layout: `direction` (`"row"`/`"column"`), `gap`, `padding` (`8`, `[v, h]` ou
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
- - Texto (herdados pelos filhos): `color`, `fontSize`, `bold`. Cursor: `cursor="pointer"`.
44
- - Cores são strings hex (`"#3584e4"`, `"#00000010"` com alfa) ou `"transparent"`.
45
- - Eventos em qualquer contêiner: `onClick`, `onPointerDown/Up/Enter/Leave`,
46
- `onPointerMove`, `onScroll`, `onKeyDown`, `onFocus`, `onBlur`. Eventos de ponteiro trazem
47
- `x`/`y` (janela) e `localX`/`localY` (relativos ao nó); enquanto o botão está pressionado,
48
- `onPointerMove` continua chegando ao nó onde o clique começou, mesmo fora dele (arrastar,
49
- sliders). `onKeyDown` só chega pelo caminho do elemento focado.
50
- - Qualquer prop aceita valor fixo ou função: `background={() => (sel() ? a : b)}`.
51
-
52
- ## Regras do layout (flexbox simplificado)
53
-
54
- - `align` padrão é **stretch**: filhos ocupam toda a largura de uma coluna. Use
55
- `align="start"`/`"center"` para encolher ao conteúdo.
56
- - `justify`: `start`, `center`, `end`, `space-between`. Não existe `space-around`/`evenly`.
57
- - `grow={1}` divide o espaço que sobra. Sem `grow`, o tamanho é o do conteúdo.
58
- - Um filho **não encolhe abaixo do próprio conteúdo** (como `min-size: auto` do CSS), exceto
59
- `Scroll`, `clip` ou `minWidth/minHeight` explícito. Lista longa dentro de uma coluna
60
- que cresce sem parar? Envolva em `<Scroll grow={1}>`.
61
- - Para um `Scroll` dentro de uma coluna com `grow`, a coluna também precisa de `minHeight={0}`.
62
- - Não há porcentagens, `flex-wrap`, grid nem quebra de linha de texto: monte linhas com `Row`
63
- e larguras fixas (veja tabelas: `Text width={110}`) ou `grow`.
64
-
65
- ## Tema e janela
66
-
67
- - Cores e tamanhos padrão em `theme` (`theme.text.secondary`, `theme.accent.base`,
68
- `theme.danger.base`, `theme.divider`...). Mude `theme` antes de `run()` para personalizar.
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()` são accessors.
71
- - Ainda não existem: tema escuro, animações, quebra de linha, seleção de texto, IME,
72
- várias janelas.
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.
@@ -1,34 +1,37 @@
1
1
  # AGENTS.md
2
2
 
3
- App nodebuilder: UI nativa para Linux em TSX, **sem DOM e sem React**. Leia antes de mudar código.
3
+ nodebuilder app: native Linux UI in TSX, **no DOM and no React**. Read this before changing code.
4
4
 
5
- ## Comandos
6
- - `bun run dev` abre o app; `bun test` roda os testes sem janela; `bun run typecheck` checa tipos.
7
- - Antes de concluir: `bun run typecheck && bun test`.
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
- ## Modelo (igual ao SolidJS, não ao React)
10
- - **Componentes rodam uma vez.** Não há re-render nem hooks. Estado é `createSignal`;
11
- derivados são `createMemo`; efeitos são `createEffect` (+ `onCleanup`).
12
- - **Só funções são reativas.** `<Text>{count}</Text>` ou `text={() => ...}` atualizam;
13
- `<Text>{count()}</Text>` é lido uma vez e nunca muda. O mesmo vale para estilo:
14
- `background={() => (ativo() ? "#3584e4" : "#0000")}`.
15
- - Listas: `<For each={lista}>{(item) => ...}</For>` (chave por referência). Condicional:
16
- `<Show when={() => ...} fallback={...}>`.
17
- - Estilo são props planas (`padding`, `gap`, `grow`, `background`...), não CSS. Não existem
18
- `className`, `style={{}}`, `div`, `useState` nem bibliotecas de React (ex.: `lucide-react`).
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 (detalhes por assunto)
21
- Em `.agents/skills/<nome>/SKILL.md` (com links em `.claude/skills/` para o Claude Code). Agentes
22
- que suportam skills as carregam sozinhos; os outros devem ler a skill do assunto antes de começar:
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 | Quando |
27
+ | Skill | When |
25
28
  |---|---|
26
- | `nodebuilder-ui` | telas, widgets, props de estilo, layout, `For`/`Show`, tema |
27
- | `nodebuilder-sem-react` | antes de instalar bibliotecas ou portar código React/web |
28
- | `nodebuilder-imagens` | imagens (PNG, JPEG, WebP, AVIF), arquivos no app |
29
- | `nodebuilder-icones` | ícones (Lucide, Phosphor) e SVG com `Icon` |
30
- | `nodebuilder-graficos` | gráficos prontos (`@nodebuilderjs/core/charts`, API do Recharts) |
31
- | `nodebuilder-desenho` | desenho livre com `Canvas` |
32
- | `nodebuilder-testes` | testes sem janela (`mount`, `findByText`, `click`) |
33
- | `nodebuilder-empacotar` | AppImage/.deb/.rpm, arquivos embutidos, ícone e app id |
34
- | `nodebuilder-desempenho` | lentidão, listas grandes, dados ao vivo, animação |
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 |
@@ -1,32 +1,32 @@
1
1
  # {{title}}
2
2
 
3
- App desktop nativo para Linux feito com [nodebuilder](https://www.npmjs.com/package/@nodebuilderjs/core):
4
- componentes TSX com reatividade estilo SolidJS, desenhados por um núcleo nativo (Wayland/X11 + GLES).
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 # abre a janela e reinicia ao salvar
8
- bun start # roda uma vez
9
- bun test # testes sem janela (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 e .tar.gz em dist/
11
+ bun run package # AppImage, .deb, .rpm and .tar.gz in dist/
12
12
  ```
13
13
 
14
- ## Estrutura
14
+ ## Structure
15
15
 
16
16
  ```
17
17
  src/
18
- main.tsx abre a janela: run(() => <App />)
19
- App.tsx layout da tela; cria o estado e o passa aos componentes
20
- estado.ts signals e ações do app (contador, tarefas, histórico)
21
- icones.ts ícones SVG usados pelo app
22
- componentes/
23
- Contador.tsx botões e texto reativo
24
- Tarefas.tsx campo de texto, lista com For/Checkbox, Show
25
- Atividade.tsx gráfico (@nodebuilderjs/core/charts) que acompanha o estado
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 testes sem janela: mount + interact (clicar, digitar)
27
+ App.test.tsx tests without a window: mount + interact (click, type)
28
28
  ```
29
29
 
30
- Para apagar o exemplo e começar do zero, deixe só a `<Window>` em `App.tsx`.
30
+ To remove the example and start from scratch, keep only the `<Window>` in `App.tsx`.
31
31
 
32
- Requisitos: Linux com Wayland ou X11, [Bun](https://bun.sh) 1.4+.
32
+ Requirements: Linux with Wayland or X11, [Bun](https://bun.sh) 1.4+.
@@ -1,29 +1,32 @@
1
- import { Column, Icon, Row, Scroll, Text, Window, theme } from "@nodebuilderjs/core";
2
- import { Atividade } from "./componentes/Atividade.js";
3
- import { Contador } from "./componentes/Contador.js";
4
- import { Tarefas } from "./componentes/Tarefas.js";
5
- import { criarEstado } from "./estado.js";
6
- import { brilho } from "./icones.js";
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 estado = criarEstado();
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
- <Icon svg={brilho} size={32} color={theme.accent.base} />
15
- <Column gap={2}>
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}>Edite src/App.tsx e salve: o bun run dev reabre a janela.</Text>
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
- <Contador estado={estado} />
23
- <Atividade estado={estado} />
25
+ <Counter state={state} />
26
+ <Activity state={state} />
24
27
  </Column>
25
28
  <Column grow={1}>
26
- <Tarefas estado={estado} />
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
+ }