create-nodebuilderjs 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/index.js +12 -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 +89 -0
  5. package/template/.agents/skills/nodebuilder-from-react/SKILL.md +80 -0
  6. package/template/.agents/skills/nodebuilder-icons/SKILL.md +95 -0
  7. package/template/.agents/skills/nodebuilder-images/SKILL.md +56 -0
  8. package/template/.agents/skills/nodebuilder-packaging/SKILL.md +37 -0
  9. package/template/.agents/skills/nodebuilder-performance/SKILL.md +59 -0
  10. package/template/.agents/skills/nodebuilder-testing/SKILL.md +199 -0
  11. package/template/.agents/skills/nodebuilder-ui/SKILL.md +535 -58
  12. package/template/AGENTS.md +34 -27
  13. package/template/README.md +19 -18
  14. package/template/_gitignore +1 -0
  15. package/template/src/App.tsx +16 -13
  16. package/template/src/components/Activity.tsx +24 -0
  17. package/template/src/components/Counter.tsx +24 -0
  18. package/template/src/components/Tasks.tsx +54 -0
  19. package/template/src/icons.ts +10 -0
  20. package/template/src/state.ts +50 -0
  21. package/template/test/App.test.tsx +45 -35
  22. package/template/.agents/skills/nodebuilder-desempenho/SKILL.md +0 -36
  23. package/template/.agents/skills/nodebuilder-desenho/SKILL.md +0 -54
  24. package/template/.agents/skills/nodebuilder-empacotar/SKILL.md +0 -37
  25. package/template/.agents/skills/nodebuilder-graficos/SKILL.md +0 -87
  26. package/template/.agents/skills/nodebuilder-icones/SKILL.md +0 -53
  27. package/template/.agents/skills/nodebuilder-imagens/SKILL.md +0 -50
  28. package/template/.agents/skills/nodebuilder-sem-react/SKILL.md +0 -48
  29. package/template/.agents/skills/nodebuilder-testes/SKILL.md +0 -56
  30. package/template/src/componentes/Atividade.tsx +0 -24
  31. package/template/src/componentes/Contador.tsx +0 -22
  32. package/template/src/componentes/Tarefas.tsx +0 -38
  33. package/template/src/estado.ts +0 -50
  34. package/template/src/icones.ts +0 -11
@@ -1,72 +1,549 @@
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 (gradient backgrounds, Window, custom title bar with dragRegion and WindowControls, 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, animate, easing, mixColor), equal-width columns, absolute positioning and stacking (zIndex), clip with rounded corners, shadows, events (double/right click, key names), reusable components (MaybeAccessor, read, batch), 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), `decorations={false}` (your own title bar, see "Theme and window") |
16
+ | `WindowControls` | minimize, maximize/restore and close buttons, for a custom title bar | style (`gap`...) |
17
+ | `Column` / `Row` / `Box` | flex container (column, row; `Box` is a column) | style + events |
18
+ | `Grid` | children in rows of equal-width cells (cards, covers, galleries) | `columns`, `gap`, `rowGap` (default `gap`); see "Layout rules" |
19
+ | `Scroll` | scrolling: vertical (default), `horizontal` or `horizontal vertical` (both) | needs `grow={1}` or a size; see "Scrolling" below |
20
+ | `Text` | text; wraps at spaces to fit the width | children (`{count}`, `"a"`) or `text={() => ...}`, `wrap` (default `true`), `maxLines` (truncates with "…"), `textAlign`, `lineHeight`, `underline`, `selectable` |
21
+ | `Image` | PNG, JPEG, WebP, AVIF | see the `nodebuilder-images` skill |
22
+ | `Icon` | SVG icon (Lucide, Phosphor), color inherited like text | `svg`, `size` (20), `color`, `strokeWidth`; skill `nodebuilder-icons` |
23
+ | `Canvas` | free drawing: lines, polygons, arcs, text | `draw={(g) => ...}` + size; skill `nodebuilder-drawing` |
24
+ | `Button` | button with focus and keyboard | `label` or children, `variant` (`default`, `primary`, `danger`, `flat`), `disabled`, `onClick` |
25
+ | `TextField` | controlled text field | `value` (accessor), `onInput(value)`, `onSubmit`, `placeholder`, `disabled` |
26
+ | `Checkbox` | checkbox | `checked` (accessor), `onChange(checked)`, `label`, `disabled` |
27
+ | `Toggle` | on/off switch (`Switch` is the control-flow component) | `checked` (accessor), `onChange(on)`, `label`, `disabled` |
28
+ | `RadioGroup` | one option among several; a single Tab stop, arrows switch | `value` (accessor), `onChange(v)`, `options=[{ value, label, disabled? }]`, `disabled` |
29
+ | `Slider` | number by dragging or with arrows/PageUp/Home/End | `value` (accessor), `onChange(n)`, `min` (0), `max` (100), `step` (1), `width` (200), `disabled` |
30
+ | `Select` | dropdown list anchored to the field; full keyboard support | `value` (accessor, may be `undefined`), `onChange(v)`, `options=[{ value, label, disabled? }]`, `placeholder`, `width`, `disabled` |
31
+ | `Tabs` | tab bar (the content is yours, with `<Switch>`) | `value` (accessor), `onChange(v)`, `tabs=[{ value, label, disabled? }]` |
32
+ | `Tooltip` | hint when the pointer rests over the child | `text`, `placement` (`"top"`), `delay` (500 ms), child |
33
+ | `Dialog` | ready-made modal dialog: title, body, actions | `open` (accessor), `onClose`, `title`, `actions`, `width` (420), `dismissible` (true) |
34
+ | `ProgressBar` | progress bar, 6 px | `value` from 0 to 1 (without it: indeterminate) |
35
+ | `Spinner` | busy, no measure of progress | `size` (20), `color` |
36
+ | `Card` | column with background, border, radius 12, padding 16, gap 8 | style |
37
+ | `Divider` / `Spacer` | 1 px line / flexible space (optional fixed `size`) | |
38
+ | `<For each={list}>{(item, i) => ...}</For>` | list keyed by reference; `i` is an accessor | `fallback` |
39
+ | `<Index each={list}>{(item, i) => ...}</Index>` | list by position (primitives); `item` is an accessor, `i` a number | `fallback` |
40
+ | `<Show when={() => ...} fallback={...}>` | conditional; a `{(v) => ...}` child gets the value as an accessor | |
41
+ | `<Switch fallback={...}><Match when={...}>...</Match></Switch>` | several cases: the first true `Match` | `Match` accepts `{(v) => ...}` |
42
+ | `<ErrorBoundary fallback={(error, reset) => ...}>` | replaces children that threw with the `fallback` | `reset()` tries again |
29
43
 
30
44
  ```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} />
45
+ const [name, setName] = createSignal("");
46
+ <TextField value={name} onInput={setName} onSubmit={save} placeholder="Name" />
47
+ <Checkbox checked={active} onChange={setActive} label="Active" />
48
+ <Button label="Save" variant="primary" disabled={() => !name()} onClick={save} />
49
+ <Toggle checked={wifi} onChange={setWifi} label="Wi-Fi" />
50
+ <Slider value={volume} onChange={setVolume} max={100} step={5} />
35
51
  ```
36
52
 
37
- ## Props de estilo (qualquer widget)
38
-
39
- - Layout: `direction` (`"row"`/`"column"`), `gap`, `padding` (`8`, `[v, h]` ou
40
- `[top, right, bottom, left]`), `width`, `height`, `minWidth`, `minHeight`, `maxWidth`,
41
- `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.
53
+ `disabled` (value or function) on any control: dims it, removes it from Tab order and focus, and
54
+ ignores clicks and keys. Do not fake it with `opacity` + an empty `onClick`.
55
+
56
+ ## Style props (any widget)
57
+
58
+ - Layout: `direction` (`"row"`/`"column"`), `gap`, `padding` (`8`, `[v, h]` or
59
+ `[top, right, bottom, left]`), `margin` (same forms, negative allowed), `width`, `height`, `minWidth`, `minHeight`, `maxWidth`,
60
+ `maxHeight`, `grow`, `aspectRatio` (width / height), `align`, `alignSelf`, `justify`,
61
+ `wrap` (on a `Row`/`Column`: lines; on a `Text`: word wrapping), `rowGap`,
62
+ `position: "absolute"` + `left`/`top`/`right`/`bottom`, `zIndex`, `clip`.
63
+ - Visual: `background`, `radius`, `borderWidth`, `borderColor`, `shadow={{ color, blur, y }}`,
64
+ `opacity` (0 to 1, applies to children), `translateX`/`translateY` (shifts after
65
+ layout, without pushing siblings; for animation).
66
+ - **Gradient background** (CSS `linear-gradient`): `background={{ linear: ["#1db954", "#121212"], angle: 180 }}`.
67
+ `angle` in degrees (0 = to top, 90 = to right, default 180 = to bottom); stops are colors or
68
+ `[color, offset]` (0..1, missing ones spread evenly). Works with `radius`, borders, `opacity`,
69
+ `hoverStyle`, and reactively: `background={() => ({ linear: [theme.accent.base, "transparent"] })}`.
70
+ Interpolated premultiplied (fading to `"transparent"` stays the same hue). Linear only (no
71
+ radial/conic); `borderColor`, `color` and `shadow` take solid colors only.
72
+ - `clip` cuts the children to the inside of the border, **rounded** (radius `radius - borderWidth`):
73
+ `<Card clip padding={0}>` + an `Image` or a colored header gets the card's rounded corners.
74
+ - `shadow` is the node's rounded box, blurred `blur` px on each side of its edge and moved down
75
+ by `y` (no x offset, no spread). It is drawn even without `background`: then the whole blurred
76
+ shape shows (a soft glow), and a translucent background lets it show through.
77
+ - Text (inherited by children): `color`, `fontSize`, `bold`, `fontWeight` (1..1000 like CSS:
78
+ 300 light, 500 medium, 700 bold, 900 black; `bold` is 700 and `fontWeight` wins on the same
79
+ widget), `italic`, `letterSpacing` (px after every character, may be negative: `letterSpacing={1.5}`
80
+ on small uppercase labels), `underline` (a line under each line of text, in its color;
81
+ `underline={false}` turns it off below; `TextField` ignores it). Weights come from the
82
+ system font (the closest it has).
83
+ Symbols, CJK and emoji the UI font lacks come from the system's fallback fonts; emoji are in
84
+ color (`color` does not tint them, `opacity` fades them). Cursor: `cursor="pointer"`. A link that underlines on hover:
85
+
86
+ ```tsx
87
+ import { Text, createSignal, theme } from "@nodebuilderjs/core";
88
+
89
+ const [hovered, setHovered] = createSignal(false);
90
+ <Text color={() => theme.accent.base} underline={hovered} cursor="pointer"
91
+ onPointerEnter={() => setHovered(true)} onPointerLeave={() => setHovered(false)}
92
+ onClick={() => console.log("open docs")}>Read the docs</Text>;
93
+ ```
94
+ - Colors are hex strings (`"#3584e4"`, `"#00000010"` with alpha) or `"transparent"`.
95
+ - Events on any container: `onClick`, `onPointerDown/Up/Enter/Leave`,
96
+ `onPointerMove`, `onScroll`, `onKeyDown`, `onFocus`, `onBlur`. Pointer events carry
97
+ `x`/`y` (window), `localX`/`localY` (relative to the node), `button` (0 left, 1 middle,
98
+ 2 right) and `clicks` (2 = double click); while the button is held,
99
+ `onPointerMove` keeps reaching the node where the press started, even outside it (dragging,
100
+ sliders). `onKeyDown` only arrives along the path of the focused element; it gets `key`
101
+ (xkb names: `"Return"`, `"Escape"`, `"Tab"`, `"BackSpace"`, `"space"`, `"Up"`, `"Left"`, `"a"`),
102
+ `text`, `ctrl`, `shift`, `alt`, `repeat`. `onScroll` gets `dx`/`dy`. `onClick` is the left button only
103
+ (context menu: `onPointerDown` with `e.button === 2`) and runs only on the innermost node that
104
+ has one (a button inside a clickable card clicks only the button); the other events go up
105
+ through the parents until a handler calls `e.stop()`, also past nodes with `onClick` (a
106
+ list's `onPointerDown` with `e.button === 2` sees a right press on any of its clickable
107
+ rows; on keys and the wheel, `stop()` also keeps the surrounding `Scroll` still).
108
+ - Any prop accepts a fixed value or a function: `background={() => (sel() ? a : b)}`.
109
+ - `onResize={(s) => ...}`: the node's size (`s.width`, `s.height`) after layout, the first
110
+ time and whenever it changes. Use it to adapt the screen to the available width.
111
+ - `ref={(el) => ...}` on any widget: `el.focus()`, `el.blur()`, `el.scrollIntoView()`,
112
+ on a `Scroll` `el.scrollTo(y)` / `el.scrollTo({ x, y })` and `el.scrollX`/`el.scrollY`, and
113
+ `el.x`/`y`/`width`/`height` from the last layout. It is called on creation, before layout:
114
+ keep the `el` and read the box later (in a click, for example).
115
+ - `ariaLabel="Play"` on any widget names an element without text (an icon-only button);
116
+ tests find it with `findByLabel`. A `Tooltip` gives its tip to a trigger that has none.
117
+
118
+ ## Hover and press
119
+
120
+ Do not write `onPointerEnter`/`onPointerLeave` + a signal for hover effects. Use
121
+ `hoverStyle`/`pressStyle` (any widget, any style prop, values fixed or functions), and
122
+ `createInteraction()` when something else depends on the state (a child shown while its
123
+ parent is hovered):
124
+
125
+ ```tsx
126
+ import { Box, Button, Column, Show, Text, createInteraction } from "@nodebuilderjs/core";
127
+
128
+ function SongCard(props: { title: string; onPlay: () => void }) {
129
+ const card = createInteraction();
130
+ return (
131
+ <Box {...card.props} direction="row" align="center" gap={12} padding={12} radius={8}
132
+ background="#181818" hoverStyle={{ background: "#282828" }} pressStyle={{ opacity: 0.85 }}>
133
+ <Column grow={1}>
134
+ <Text color="#ffffff">{props.title}</Text>
135
+ </Column>
136
+ <Show when={card.hovered}>
137
+ <Button label="Play" variant="primary" onClick={props.onPlay} />
138
+ </Show>
139
+ </Box>
140
+ );
141
+ }
142
+ ```
143
+
144
+ - `pressStyle` wins over `hoverStyle`; on leave the base values come back (a base given as a
145
+ function keeps updating underneath). They win over a widget's own hover colors (`Button`);
146
+ a `disabled` control ignores them.
147
+ - `createInteraction()` returns `hovered`, `pressed`, `focused` (accessors) and `props`.
148
+ `{...card.props}` adds to the widget's handlers and to your own `onPointerEnter`... written
149
+ before or after it (nothing is overwritten); the card stays hovered while the pointer is
150
+ over its children. `focused` only changes on focusable widgets (`Button`, `TextField`).
151
+ - `hoverStyle` alone does not take the press; `pressStyle` and `card.props` do, so inside a
152
+ window's drag region (title bar) such a widget no longer drags the window.
153
+ - Like CSS `:active`, a parent's `pressStyle` (and `pressed()`) also turns on while a child
154
+ inside it (a button in the card) is pressed; `onPointerDown={(e) => e.stop()}` on the child
155
+ (or on a `Box` around a `Button`) keeps the press to the child.
156
+
157
+ ## Conditions, lists and errors
158
+
159
+ ```tsx
160
+ import { Button, ErrorBoundary, Index, Match, Show, Switch, Text, createSignal } from "@nodebuilderjs/core";
161
+
162
+ const [order, setOrder] = createSignal<{ id: number } | null>(null);
163
+ const [step, setStep] = createSignal<"cart" | "payment" | "done">("cart");
164
+ const [grades] = createSignal([7, 9, 10]);
165
+
166
+ <Show when={order} fallback={<Text>No order</Text>}>
167
+ {(o) => <Text>{() => `Order ${o().id}`}</Text>}
168
+ </Show>;
169
+
170
+ <Switch fallback={<Text>Thank you!</Text>}>
171
+ <Match when={() => step() === "cart"}><Text>Your cart</Text></Match>
172
+ <Match when={() => step() === "payment"}><Text>Payment</Text></Match>
173
+ </Switch>;
174
+
175
+ <Index each={grades}>{(grade, i) => <Text>{() => `Test ${i + 1}: ${grade()}`}</Text>}</Index>;
176
+
177
+ <ErrorBoundary fallback={(error, reset) => <Button label="Try again" onClick={reset} />}>
178
+ <Text>{() => `Order ${order()!.id}`}</Text>
179
+ </ErrorBoundary>;
180
+ ```
181
+
182
+ - `<For>` vs `<Index>`: `For` tracks each **object** (the row survives reordering; recreating
183
+ the objects recreates the rows). `Index` tracks each **position**: the row stays and the
184
+ `item()` accessor changes; good for lists of numbers/strings or data that always arrives as new
185
+ objects.
186
+ - A function child of `Show`/`Match` gets the value only when it takes a parameter: `{(o) => ...}`
187
+ gets the value as an accessor; `{() => ...}` (no parameter) is a plain reactive child.
188
+ - `Switch` only accepts `Match` children, written in JSX (not generated by a function).
189
+ - `ErrorBoundary` catches errors while creating the children and later in their effects (e.g. a click
190
+ changes a signal and a text throws). It disposes everything the children created and shows the
191
+ `fallback`; `reset()` recreates the children. What the `onClick` itself throws is not caught:
192
+ handle it with `try/catch` in the handler.
193
+
194
+ ## Context, ref and size
195
+
196
+ ```tsx
197
+ import { Column, Scroll, TextField, Text, createContext, createSignal, useContext } from "@nodebuilderjs/core";
198
+ import type { Accessor, ElementRef } from "@nodebuilderjs/core";
199
+
200
+ // Value shared through the tree (theme, language, logged-in user), without passing props.
201
+ const User = createContext<Accessor<string>>(() => "anonymous");
202
+ function Greeting() {
203
+ const user = useContext(User);
204
+ return <Text>{() => `Hello, ${user()}`}</Text>;
205
+ }
206
+ const [user, setUser] = createSignal("Ana");
207
+ <User.Provider value={user}><Greeting /></User.Provider>;
208
+
209
+ // Focus a field on open and scroll a list to the end.
210
+ const [search, setSearch] = createSignal("");
211
+ let list: ElementRef | undefined;
212
+ <Column grow={1}>
213
+ <TextField ref={(el) => el.focus()} value={search} onInput={setSearch} />
214
+ <Scroll ref={(el) => (list = el)} grow={1}>{/* ... */}</Scroll>
215
+ </Column>;
216
+ const scrollToEnd = () => list?.scrollTo(Infinity);
217
+ ```
218
+
219
+ - The context value is read once, when the component runs. For a value that changes, pass
220
+ an accessor (signal) and call it inside a reactive function, as above.
221
+ - `useContext` only works in the component body (not inside an `onClick` or `setTimeout`):
222
+ read it there and keep it in a variable.
223
+
224
+ ## Your own components
225
+
226
+ ```tsx
227
+ import { Column, Row, Text, batch, createSignal, read } from "@nodebuilderjs/core";
228
+ import type { Child, MaybeAccessor } from "@nodebuilderjs/core";
229
+
230
+ // A prop that may be fixed or reactive: MaybeAccessor + read() inside a function.
231
+ function Stat(props: { label: string; value: MaybeAccessor<number>; children?: Child }) {
232
+ return (
233
+ <Row gap={8} align="center">
234
+ <Text>{props.label}</Text>
235
+ <Text bold>{() => read(props.value).toLocaleString()}</Text>
236
+ {props.children}
237
+ </Row>
238
+ );
239
+ }
240
+
241
+ const [plays, setPlays] = createSignal(0);
242
+ const [likes, setLikes] = createSignal(0);
243
+ <Column>
244
+ <Stat label="Plays" value={plays} />
245
+ <Stat label="Year" value={2024} />
246
+ </Column>;
247
+ batch(() => { setPlays(1200); setLikes(80); }); // several writes, effects run once at the end
248
+ ```
249
+
250
+ - `untrack(() => ...)` reads signals without subscribing; `createMemo(() => ...)` caches a
251
+ derived value; `StyleProps` (type) is every style prop, to accept and pass them on.
252
+
253
+ ## Scrolling
254
+
255
+ ```tsx
256
+ import { Button, Card, For, Row, Scroll, Text } from "@nodebuilderjs/core";
257
+ import type { ElementRef } from "@nodebuilderjs/core";
258
+
259
+ const items = Array.from({ length: 200 }, (_, i) => `Item ${i}`);
260
+ const rows: ElementRef[] = [];
261
+ <>
262
+ {/* Strip that scrolls sideways: children keep their width. */}
263
+ <Scroll horizontal gap={12} padding={[0, 0, 14, 0]}>
264
+ <For each={items}>{(t) => <Card width={160}><Text>{t}</Text></Card>}</For>
265
+ </Scroll>
266
+ <Button label="Go to 150" onClick={() => rows[150]?.scrollIntoView()} />
267
+ <Scroll grow={1}>
268
+ <For each={items}>{(t, i) => <Row ref={(el) => (rows[i()] = el)} padding={8}><Text>{t}</Text></Row>}</For>
269
+ </Scroll>
270
+ {/* Wide and long table: scrolls on both axes. */}
271
+ <Scroll horizontal vertical height={300}>{/* rows with fixed widths */}</Scroll>
272
+ </>;
273
+ ```
274
+
275
+ - `<Scroll>` scrolls vertically. `horizontal`: a row whose children keep their width (text
276
+ inside does not wrap, unless given `width`/`maxWidth`). `horizontal vertical`: a column
277
+ that scrolls on both axes; each child is at least the visible width and grows if the
278
+ content asks for it.
279
+ - The user scrolls with the wheel (Shift+wheel goes sideways; touchpad on both axes), by dragging
280
+ the scrollbar thumb, by clicking the track (one page) and with the keyboard: PageUp/PageDown,
281
+ Home/End and the arrows scroll the `Scroll` around the focused element (with no focus, the one under
282
+ the mouse), unless an `onKeyDown` calls `e.stop()`. `TextField` keeps Home/End and ←/→;
283
+ ↑/↓ scroll.
284
+ - Scrollbars sit **on top of** the content, in a 14 px strip along the right edge (and the
285
+ bottom one), and only exist when there is something to scroll: leave `padding` on that edge if
286
+ buttons touch it (like the `padding={[0, 0, 14, 0]}` above).
287
+ - `el.scrollIntoView()` (on any element, via `ref`) scrolls the surrounding `Scroll`s the minimum
288
+ needed for it to show; it takes effect on the next frame, even for a just-created or off-screen
289
+ element. `el.scrollTo({ x, y })` accepts just one of the axes.
290
+ - An open `<Overlay modal>` blocks the wheel, keys and scrollbars of what is behind it.
291
+
292
+ ## Dialogs, menus and hints: `<Overlay>`
293
+
294
+ For the common cases use the ready-made widgets (`Dialog`, `Select`, `Tooltip`), which already handle
295
+ focus, keyboard and closing. `<Overlay>` is for what they do not cover (options menu, toast,
296
+ custom popover):
297
+
298
+ No `position: fixed`, and `zIndex` only orders siblings: whatever must stay on top of everything
299
+ (outside the clipping of a `Scroll` or `Card`) goes in an `<Overlay>`, almost always inside a `<Show>`.
300
+
301
+ ```tsx
302
+ import { Button, Card, Overlay, Row, Show, Text, createSignal, theme } from "@nodebuilderjs/core";
303
+ import type { ElementRef } from "@nodebuilderjs/core";
304
+
305
+ const [menu, setMenu] = createSignal(false);
306
+ const [confirm, setConfirm] = createSignal(false);
307
+ let button: ElementRef | undefined;
308
+ <>
309
+ <Button ref={(el) => (button = el)} label="Options" onClick={() => setMenu((m) => !m)} />
310
+ <Show when={menu}>
311
+ <Overlay anchor={() => button} matchWidth onDismiss={() => setMenu(false)}>
312
+ <Card padding={4} shadow={{ color: "#00000030", blur: 16, y: 4 }}>
313
+ <Button label="Delete" variant="flat" onClick={() => (setMenu(false), setConfirm(true))} />
314
+ </Card>
315
+ </Overlay>
316
+ </Show>
317
+ <Show when={confirm}>
318
+ <Overlay modal background="#00000059" align="center" justify="center" onDismiss={() => setConfirm(false)}>
319
+ <Card width={320} gap={12}>
320
+ <Text bold>Delete file?</Text>
321
+ <Row gap={8} justify="end">
322
+ <Button label="Cancel" variant="flat" onClick={() => setConfirm(false)} />
323
+ <Button label="Delete" variant="danger" onClick={() => setConfirm(false)} />
324
+ </Row>
325
+ </Card>
326
+ </Overlay>
327
+ </Show>
328
+ </>;
329
+ ```
330
+
331
+ - `anchor={() => el}` (always as a function): sits next to the element, `placement`
332
+ `"bottom"` (default), `"top"`, `"right"` or `"left"`, flips side if it does not fit, never leaves the
333
+ window; `offset` (4) and `matchWidth` (same width, for menus).
334
+ - Without `anchor`: covers the window; position it with `align`/`justify`/`padding` or with
335
+ `position="absolute"` + `left`/`top`. Empty space lets clicks through (toasts, hints).
336
+ - `modal`: blocks clicks and Tab on what is behind; `background` dims the backdrop.
337
+ - `onDismiss`: click outside (when modal, on the dimmed backdrop; never on the anchor) or Esc. Close the
338
+ `<Show>` there.
339
+ - Declare the `<Overlay>` **outside** a `Button` (next to it, as above): events inside
340
+ the overlay bubble up through the parents of the place where it was declared.
341
+ - Transition: animate `opacity` and `translateY` with a signal updated in
342
+ `requestAnimationFrame` (see `examples/overlays.tsx` in the nodebuilder repository).
343
+
344
+ ## Layout rules (simplified flexbox)
345
+
346
+ - Default `align` is **stretch**: children take the full width of a column. Use
347
+ `align="start"`/`"center"` to shrink them to their content. `alignSelf` on one child
348
+ overrides its parent's `align` (e.g. one centered button in a stretched form).
349
+ - `justify`: `start`, `center`, `end`, `space-between`. There is no `space-around`/`evenly`.
350
+ - `grow={1}` splits the leftover space. Without `grow`, the size is the content's.
351
+ - A child **does not shrink below its own content** (like CSS `min-size: auto`), except
352
+ `Scroll`, `clip` or an explicit `minWidth/minHeight`. A long list inside a column
353
+ that keeps growing? Wrap it in `<Scroll grow={1}>`.
354
+ - The content of a `Scroll` does not count toward that minimum: the column (or row) around it
355
+ shrinks without needing `minHeight={0}`/`minWidth={0}`. A `Scroll horizontal` does not
356
+ lose height (only what scrolls vertically gives up height).
357
+ - In a `Row` without room, whoever has `grow` shrinks first: the long text gets
358
+ `grow={1}` (and `maxLines={1}` to become "…"), the short labels next to it stay whole.
359
+ - **Something does not show up?** In development the app prints an overflow warning once per
360
+ node when children need more room than they get and a `clip` ancestor or the window cuts
361
+ them off: `layout: Row > Text("Artists") overflows Box(panel) by 42px (children need 342px,
362
+ 300px available)`; `debugTree(root)` marks the container `[overflow +42]` and tests read the
363
+ list with `layoutWarnings()` (see the testing skill). Fix: a `Scroll horizontal`, wrapping
364
+ text, `grow` with `width={0}`, `minWidth={0}` plus `clip` on what may be cut, or more room.
365
+ - Text wraps by itself at the width it gets (`\n` also breaks); a word longer than the line
366
+ overflows. A single line: `wrap={false}` (overflows) or `maxLines={1}` (truncates with "…").
367
+ - There are no percentages: build rows with `Row` and fixed widths (see tables:
368
+ `Text width={110}`), `grow`, proportional columns (below), `wrap` or a `Grid`.
369
+ - **`<Row wrap gap={6}>`** (CSS `flex-wrap: wrap`) for chips and tags: children that do not fit
370
+ start a new line, `rowGap` (default `gap`) between lines. Each line is its own row (`grow`,
371
+ `justify`, `alignSelf` apply inside it) and is as tall as its tallest child. The Row wraps at
372
+ whatever width it gets (stretched in a column, `grow`, a fixed `width`) and its height follows;
373
+ it can shrink down to its widest child. `<Column wrap height={...}>` breaks into columns at
374
+ its height.
375
+ - **`margin`** is space outside the border that the parent keeps free: it counts in every size
376
+ around the child (natural size, `grow`, `Grid` cells, `wrap` lines) and a stretched child
377
+ gets the room minus its margins. Prefer `gap`/`padding` on the parent for regular spacing;
378
+ use `margin` for one child (extra space above a section title) or negative values (a divider
379
+ that cancels the card's padding: `margin={[8, -16]}`). Not painted, not clickable, never
380
+ collapses, adds to `gap`. On an absolute child it adds to `left`/`top`/`right`/`bottom`.
381
+ - **`<Grid columns={3} gap={8}>`** (CSS `repeat(3, 1fr)`): children flow into rows of equal
382
+ cells, whatever their content; the last row keeps its empty cells. A row is as tall as its
383
+ tallest cell and cells stretch to it (`align`/`alignSelf` change that). `rowGap` sets the
384
+ space between rows (default `gap`). Use it with `<For>` too: adding an item reflows.
385
+ - **`aspectRatio={16 / 9}`** (width / height), like CSS `aspect-ratio`: in a column or a `Grid`
386
+ cell the height follows the width; with a fixed `height` (or stretched in a `Row` with a
387
+ height) the width follows the height. `min/max` still clamp, and children taller than the
388
+ ratio make the box taller. In a `Grid`, `aspectRatio={1}` gives square cells (covers,
389
+ thumbnails) that do not stretch to a taller neighbor.
390
+ - **Equal (or proportional) columns in one row:** `width={0} grow={1}` on each child, like CSS
391
+ `flex: 1 1 0`. The fixed width is the base `grow` adds to, so the widths follow only the
392
+ `grow` values (1:1:2...), not the content; text inside wraps to the column. Plain `grow={1}`
393
+ only shares the *leftover* space, so wider content makes a wider column. Rows of equal height
394
+ in a `Column` with a height: `height={0} grow={1}`. Content wider than its column overflows
395
+ into the next one (`clip` cuts it), and the row's own natural width is just its gaps: give it
396
+ room (stretched in a column, or `grow`).
397
+ - **`position="absolute"`** takes the child out of the flow: `left`/`top`/`right`/`bottom`
398
+ count from the parent's content box (inside its `padding`; negative values work), as in CSS.
399
+ It keeps its natural size, unless both `left` and `right` (or `top` and `bottom`) are set and
400
+ no `width` (`height`): then it stretches between them, within `min`/`max`. `right` (or
401
+ `bottom`) alone places it from that edge. It does not take space or change the parent's size.
402
+ Siblings are drawn and hit in child order (a later one covers an earlier one) unless they
403
+ have a **`zIndex`** (default 0; higher is above, ties keep child order). It only orders
404
+ siblings: a raised node stays inside its parent's place in the order (no global stacking
405
+ context; for that, an `<Overlay>`). What sticks out of the parent's box is drawn and clicked
406
+ there too, unless the parent clips (`clip`, `Scroll`).
407
+ - Inside a `Scroll`, an absolute child **scrolls with the content** (like CSS). For something
408
+ pinned to the visible area, put it next to the `Scroll` in a parent that does not scroll.
409
+
410
+ ```tsx
411
+ import { Box, Column, For, Grid, Image, Row, Text, createSignal } from "@nodebuilderjs/core";
412
+
413
+ // Square album covers, four per row, following the window width.
414
+ const [albums] = createSignal([{ title: "Blue", cover: "blue.jpg" }]);
415
+ <Grid columns={4} gap={12} rowGap={16}>
416
+ <For each={albums}>
417
+ {(a) => (
418
+ <Column gap={6}>
419
+ <Image src={a.cover} aspectRatio={1} fit="cover" radius={8} />
420
+ <Text maxLines={1}>{a.title}</Text>
421
+ </Column>
422
+ )}
423
+ </For>
424
+ </Grid>;
425
+
426
+ // Three columns in one row: 1 : 1 : 2, whatever their content (the manual alternative).
427
+ <Row gap={12}>
428
+ <Column width={0} grow={1}><Text>Now playing</Text></Column>
429
+ <Column width={0} grow={1}><Text>Up next, a much longer title that wraps</Text></Column>
430
+ <Column width={0} grow={2}><Text>Lyrics (twice as wide)</Text></Column>
431
+ </Row>;
432
+
433
+ // A layer over the whole cover and badges on its corners (declared last: on top).
434
+ <Box width={160} radius={12} clip background="#1f1f24">
435
+ <Column padding={16} gap={4}><Text color="#ffffff">Album</Text></Column>
436
+ <Box position="absolute" left={0} top={0} right={0} bottom={0} background="#00000066" />
437
+ <Text position="absolute" left={8} top={8} padding={[2, 6]} radius={8} background="#e01b24" color="#ffffff">NEW</Text>
438
+ <Text position="absolute" right={8} bottom={8} color="#ffffff">3:45</Text>
439
+ </Box>;
440
+
441
+ // A count badge sticking out of a card's corner (no `clip`, so it is drawn and clicked
442
+ // outside too); `zIndex` keeps it above the content even though it is declared first.
443
+ <Box width={160} padding={12} radius={10} background="#e8eaf2">
444
+ <Text position="absolute" right={-22} top={-22} zIndex={1} padding={[2, 8]} radius={10} background="#e5484d" color="#ffffff">3</Text>
445
+ <Box padding={8} radius={6} background="#3e63dd"><Text color="#ffffff">Inbox</Text></Box>
446
+ </Box>;
447
+ ```
448
+
449
+ Keep the `padding` on an inner child (as above) when an absolute layer must cover the padding
450
+ too: the edges count from inside the parent's `padding`.
451
+
452
+ ## Theme and window
453
+
454
+ - Default colors and sizes in `theme` (`theme.text.secondary`, `theme.accent.base`,
455
+ `theme.danger.base`, `theme.divider`...). Dark theme: `setTheme("dark")` (or `"light"`, or
456
+ `{ ...darkTheme, accent: {...} }`), at any time; `colorScheme()` tells which one is active.
457
+ - **Theme colors always inside a function** (`color={() => theme.text.secondary}`): that way they
458
+ follow theme changes. `color={theme.text.secondary}` keeps the color of that moment.
69
459
  - `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.
460
+ `windowState.width()`, `.height()`, `.maximized()`, `.activated()` are accessors.
461
+ - **Custom title bar** (navigation or search merged with the window buttons): `<Window
462
+ decorations={false}>` drops the built-in bar (border, corners and edge resizing stay);
463
+ `dragRegion` on any widget makes it move the window, and a double click maximizes. Plain
464
+ text and empty space inside it drag; buttons, fields and anything clickable, focusable or
465
+ with `onPointerDown` keep their press.
466
+
467
+ ```tsx
468
+ import { Row, Spacer, Text, TextField, Window, WindowControls, createSignal, theme, windowState } from "@nodebuilderjs/core";
469
+
470
+ const [query, setQuery] = createSignal("");
471
+ <Window title="Player" decorations={false}>
472
+ <Row dragRegion height={56} align="center" gap={12} padding={[0, 12]}>
473
+ <Text bold color={() => (windowState.activated() ? theme.text.primary : theme.text.secondary)}>Player</Text>
474
+ <TextField width={280} value={query} onInput={setQuery} placeholder="Search" />
475
+ <Spacer />
476
+ <WindowControls />
477
+ </Row>
478
+ {/* the rest of the screen */}
479
+ </Window>
480
+ ```
481
+ - Animation: see the next section.
482
+ - Not available yet: theme following the system, `scale`/`rotate`, selection in `TextField`
483
+ (and Ctrl+C in it), IME, multiple windows.
484
+
485
+ ## Animation
486
+
487
+ ```tsx
488
+ import { Box, animate, createInteraction, createSignal, createTween, easing, mixColor, theme } from "@nodebuilderjs/core";
489
+
490
+ const row = createInteraction();
491
+ // Moves smoothly toward its source; a new target starts from wherever it is now.
492
+ const t = createTween(() => (row.hovered() ? 1 : 0), { duration: 150, easing: easing.easeInOut });
493
+ <Box
494
+ {...row.props} height={56} radius={8}
495
+ background={() => mixColor(theme.card.background, "#1db954", t() * 0.2)}
496
+ translateY={() => -2 * t()}
497
+ />;
498
+
499
+ // One-off: calls back once per frame; returns a function that stops it.
500
+ const [x, setX] = createSignal(0);
501
+ const stop = animate(0, 300, setX, { duration: 400, easing: easing.linear, onDone: () => console.log("done") });
502
+ <Box width={40} height={40} translateX={x} />;
503
+ ```
504
+
505
+ - `createTween(source, { duration, easing })`: an accessor that follows `source` (a function
506
+ returning a number). `animate(from, to, onUpdate, { duration, easing, onDone })`.
507
+ `duration` in ms (default 200), `easing` default `easing.easeOut`.
508
+ - `createPresence(when, { enter, exit, easing })` (ms, defaults 180/120): `mounted` stays true
509
+ until the exit ends (use it in `<Show when={p.mounted}>`), `progress` goes 0→1→0 (for
510
+ `opacity`, `translateY`).
511
+ - `easing.linear`, `.easeIn`, `.easeOut`, `.easeInOut`, or any `(t: number) => number`.
512
+ - `mixColor(a, b, t)`: `a` blended toward `b` (t 0..1), as `"#rrggbbaa"`; any hex form works.
513
+ Premultiplied like CSS: `mixColor("#1db954", "transparent", 0.5)` is the same green at half alpha.
514
+ - Animate numbers only (`opacity`, `translateX/Y`, `width`, colors via `mixColor`). For a
515
+ custom loop, `requestAnimationFrame((time) => ...)` runs right before the next frame.
516
+ - `Dialog`, `Select`, `Tooltip` and `Toggle` already animate on their own. Without a window
517
+ (tests) every animation jumps to its end; `frames()` (skill `nodebuilder-testing`) moves time.
518
+
519
+ ## Selecting and copying text
520
+
521
+ Plain text **does not select**, on purpose (labels, buttons, titles). Text the user
522
+ may want to copy (ids, links, paths, error messages, results) gets `selectable`:
523
+
524
+ ```tsx
525
+ <Text selectable>{order.id}</Text>
526
+ <Text selectable color={theme.danger.base}>{error}</Text>
527
+ ```
528
+
529
+ With `selectable`: dragging selects, double-click takes the word, triple-click the line,
530
+ Ctrl+A everything and Ctrl+C copies; the cursor becomes the text cursor. It is not in the Tab order, and inside
531
+ the title bar (or any `dragRegion`) it selects instead of dragging the window. Selection is one `Text` at a time
532
+ (it does not span several) and there is no Shift+click or Shift+arrows yet.
533
+
534
+ The system clipboard (text only) is in `clipboard`:
535
+
536
+ ```tsx
537
+ import { Button, clipboard, createSignal } from "@nodebuilderjs/core";
538
+
539
+ const [link, setLink] = createSignal("https://example.com");
540
+ <Button label="Copy link" onClick={() => clipboard.writeText(link())} />;
541
+ <Button label="Paste" onClick={async () => setLink(await clipboard.readText())} />;
542
+ ```
543
+
544
+ - `readText()` is asynchronous (another program holds the content) and returns `""` if there
545
+ is no text.
546
+ - Copy from a click or key press (as above): on Wayland the system ignores copies that
547
+ do not come right after a user interaction (a `setTimeout` will not do).
548
+ - `TextField` already pastes with Ctrl+V / Shift+Insert (line breaks become spaces), but
549
+ does not select or copy yet.