create-nodebuilderjs 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +12 -10
- package/package.json +2 -2
- package/template/.agents/skills/nodebuilder-charts/SKILL.md +87 -0
- package/template/.agents/skills/nodebuilder-drawing/SKILL.md +89 -0
- package/template/.agents/skills/nodebuilder-from-react/SKILL.md +80 -0
- package/template/.agents/skills/nodebuilder-icons/SKILL.md +95 -0
- package/template/.agents/skills/nodebuilder-images/SKILL.md +56 -0
- package/template/.agents/skills/nodebuilder-packaging/SKILL.md +37 -0
- package/template/.agents/skills/nodebuilder-performance/SKILL.md +59 -0
- package/template/.agents/skills/nodebuilder-testing/SKILL.md +199 -0
- package/template/.agents/skills/nodebuilder-ui/SKILL.md +535 -58
- package/template/AGENTS.md +34 -27
- package/template/README.md +19 -18
- package/template/_gitignore +1 -0
- package/template/src/App.tsx +16 -13
- package/template/src/components/Activity.tsx +24 -0
- package/template/src/components/Counter.tsx +24 -0
- package/template/src/components/Tasks.tsx +54 -0
- package/template/src/icons.ts +10 -0
- package/template/src/state.ts +50 -0
- package/template/test/App.test.tsx +45 -35
- package/template/.agents/skills/nodebuilder-desempenho/SKILL.md +0 -36
- package/template/.agents/skills/nodebuilder-desenho/SKILL.md +0 -54
- package/template/.agents/skills/nodebuilder-empacotar/SKILL.md +0 -37
- package/template/.agents/skills/nodebuilder-graficos/SKILL.md +0 -87
- package/template/.agents/skills/nodebuilder-icones/SKILL.md +0 -53
- package/template/.agents/skills/nodebuilder-imagens/SKILL.md +0 -50
- package/template/.agents/skills/nodebuilder-sem-react/SKILL.md +0 -48
- package/template/.agents/skills/nodebuilder-testes/SKILL.md +0 -56
- package/template/src/componentes/Atividade.tsx +0 -24
- package/template/src/componentes/Contador.tsx +0 -22
- package/template/src/componentes/Tarefas.tsx +0 -38
- package/template/src/estado.ts +0 -50
- package/template/src/icones.ts +0 -11
|
@@ -1,72 +1,549 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nodebuilder-ui
|
|
3
|
-
description:
|
|
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
|
-
#
|
|
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
|
-
| `
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
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), `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 [
|
|
32
|
-
<TextField value={
|
|
33
|
-
<Checkbox checked={
|
|
34
|
-
<Button label="
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`
|
|
47
|
-
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`
|
|
56
|
-
- `
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`
|
|
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()`
|
|
71
|
-
-
|
|
72
|
-
|
|
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.
|