@trendr/core 0.1.0 → 0.2.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/README.md +368 -54
- package/index.js +7 -1
- package/package.json +21 -3
- package/src/animation.js +204 -0
- package/src/ansi.js +3 -0
- package/src/buffer.js +49 -15
- package/src/button.js +10 -1
- package/src/checkbox.js +12 -3
- package/src/diff.js +115 -12
- package/src/hooks.js +142 -2
- package/src/input.js +59 -7
- package/src/layout.js +90 -18
- package/src/list.js +139 -16
- package/src/progress.js +57 -9
- package/src/radio.js +16 -2
- package/src/renderer.js +336 -63
- package/src/scroll-box.js +105 -0
- package/src/scrollable-text.js +28 -6
- package/src/select.js +188 -68
- package/src/shimmer.js +95 -0
- package/src/signal.js +12 -0
- package/src/spinner.js +13 -4
- package/src/split-pane.js +66 -0
- package/src/table.js +46 -19
- package/src/task.js +43 -0
- package/src/text-area.js +8 -5
- package/src/text-input.js +9 -6
- package/src/toast.js +4 -6
- package/src/wrap.js +110 -11
package/README.md
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<pre align="center">
|
|
2
|
+
▗ ▌
|
|
3
|
+
▜▘▛▘█▌▛▌▛▌
|
|
4
|
+
▐▖▌ ▙▖▌▌▙▌
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
</pre>
|
|
6
7
|
|
|
7
|
-
Direct-mode TUI renderer with JSX, signals, and per-cell diffing.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
JSX components, signals, per-cell diffing and flexbox without React and Yoga. Terminals are character grids, not DOM trees. Why reconcile a virtual DOM to write escape sequences?
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
{ "jsx": "automatic", "jsxImportSource": "trend" }
|
|
13
|
-
```
|
|
11
|
+
4-16x faster frame times and 580x less I/O per render than popular TUI frameworks. No dependencies. [benchmarks](bench/README.md)
|
|
14
12
|
|
|
15
|
-
https://github.com/user-attachments/assets/
|
|
13
|
+
https://github.com/user-attachments/assets/70c91ab3-659a-4bb0-939a-961dcfbaba61
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
## Usage
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
Requires esbuild (or similar) for JSX transformation.
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
```json
|
|
20
|
+
{ "jsx": "automatic", "jsxImportSource": "trend" }
|
|
21
|
+
```
|
|
22
22
|
|
|
23
23
|
```jsx
|
|
24
|
-
import { mount, createSignal, useInput } from '
|
|
24
|
+
import { mount, createSignal, useInput } from '@trendr/core'
|
|
25
25
|
|
|
26
26
|
function App() {
|
|
27
27
|
const [count, setCount] = createSignal(0)
|
|
@@ -42,20 +42,31 @@ function App() {
|
|
|
42
42
|
mount(App)
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
`mount(Component, { stream?, stdin?, theme? })` enters alt screen
|
|
45
|
+
`mount(Component, { stream?, stdin?, title?, theme? })` enters alt screen and returns `{ unmount, repaint }`. Renders on demand when signals change, capped at 60fps.
|
|
46
46
|
|
|
47
47
|
### Theming
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Pass a theme object to `mount` to configure global defaults:
|
|
50
50
|
|
|
51
51
|
```jsx
|
|
52
|
-
mount(App, {
|
|
52
|
+
mount(App, {
|
|
53
|
+
theme: {
|
|
54
|
+
accent: 'green', // focus/highlight color, default 'cyan'
|
|
55
|
+
cursor: {
|
|
56
|
+
blink: true, // default false
|
|
57
|
+
rate: 530, // blink interval ms, default 530
|
|
58
|
+
style: 'block', // default 'block'
|
|
59
|
+
bg: 'cyan', // cursor background color
|
|
60
|
+
color: 'black', // cursor text color
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
})
|
|
53
64
|
```
|
|
54
65
|
|
|
55
|
-
Components read the
|
|
66
|
+
Components read the theme with `useTheme()`:
|
|
56
67
|
|
|
57
68
|
```jsx
|
|
58
|
-
import { useTheme } from '
|
|
69
|
+
import { useTheme } from '@trendr/core'
|
|
59
70
|
|
|
60
71
|
const { accent } = useTheme()
|
|
61
72
|
```
|
|
@@ -65,7 +76,7 @@ Individual components still accept explicit color props (e.g. `<Spinner color="m
|
|
|
65
76
|
## Signals
|
|
66
77
|
|
|
67
78
|
```js
|
|
68
|
-
import { createSignal, createEffect, createMemo, batch, untrack, onCleanup } from '
|
|
79
|
+
import { createSignal, createEffect, createMemo, batch, untrack, onCleanup } from '@trendr/core'
|
|
69
80
|
|
|
70
81
|
const [value, setValue] = createSignal(0)
|
|
71
82
|
value() // read (tracks dependency)
|
|
@@ -109,26 +120,58 @@ Two element types: `box` (container) and `text` (leaf).
|
|
|
109
120
|
margin: 1, // same variants as padding
|
|
110
121
|
border: 'round', // 'single' | 'double' | 'round' | 'bold'
|
|
111
122
|
borderColor: 'cyan',
|
|
123
|
+
borderEdges: { bottom: true, left: true }, // render only specific sides
|
|
112
124
|
bg: 'blue', // background color
|
|
125
|
+
texture: 'dots', // background texture (see below)
|
|
126
|
+
textureColor: '#333', // color for texture characters
|
|
127
|
+
position: 'absolute', // remove from flow, position with top/left/right/bottom
|
|
128
|
+
top: 0, left: 0, right: 0, bottom: 0,
|
|
129
|
+
overflow: 'scroll', // scrollable container (see ScrollBox)
|
|
130
|
+
scrollOffset: 0, // scroll position (rows from top)
|
|
113
131
|
}}>
|
|
132
|
+
```
|
|
114
133
|
|
|
134
|
+
```jsx
|
|
115
135
|
<text style={{
|
|
116
136
|
color: 'cyan', // named, hex (#ff0000), or 256-color index
|
|
117
137
|
bg: 'black',
|
|
118
|
-
bold: true,
|
|
119
|
-
|
|
120
|
-
italic: true,
|
|
121
|
-
underline: true,
|
|
122
|
-
inverse: true,
|
|
123
|
-
strikethrough: true,
|
|
138
|
+
bold: true, dim: true, italic: true,
|
|
139
|
+
underline: true, inverse: true, strikethrough: true,
|
|
124
140
|
overflow: 'wrap', // 'wrap' (default) | 'truncate' | 'nowrap'
|
|
125
141
|
}}>
|
|
126
142
|
```
|
|
127
143
|
|
|
144
|
+
### Background Textures
|
|
145
|
+
|
|
146
|
+
Repeating character fill for box backgrounds. Works with or without `bg`.
|
|
147
|
+
|
|
148
|
+
```jsx
|
|
149
|
+
<box style={{ bg: '#1a1a2e', texture: 'dots', textureColor: '#2a2a4e' }}>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Presets: `'shade-light'` (░), `'shade-medium'` (▒), `'shade-heavy'` (▓), `'dots'` (·), `'cross'` (╳), `'grid'` (┼), `'dash'` (╌). Or pass any single character: `texture: '~'`.
|
|
153
|
+
|
|
154
|
+
Texture characters show through spaces in text rendered on top (unless the text has an explicit `bg`, which claims the cell).
|
|
155
|
+
|
|
156
|
+
### Absolute Positioning
|
|
157
|
+
|
|
158
|
+
Position relative to parent, removed from flex flow.
|
|
159
|
+
|
|
160
|
+
```jsx
|
|
161
|
+
<box style={{ border: 'round', height: 5, flexDirection: 'column' }}>
|
|
162
|
+
<text>content here</text>
|
|
163
|
+
<box style={{ position: 'absolute', top: 0, right: 1 }}>
|
|
164
|
+
<text style={{ color: 'green', bold: true }}>ONLINE</text>
|
|
165
|
+
</box>
|
|
166
|
+
</box>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
If both `left` and `right` are set, width is derived (same for `top`/`bottom`).
|
|
170
|
+
|
|
128
171
|
`Box`, `Text`, and `Spacer` are convenience wrappers:
|
|
129
172
|
|
|
130
173
|
```jsx
|
|
131
|
-
import { Box, Text, Spacer } from '
|
|
174
|
+
import { Box, Text, Spacer } from '@trendr/core'
|
|
132
175
|
<Box style={{ flexDirection: 'row' }}><Text>hello</Text><Spacer /><Text>right</Text></Box>
|
|
133
176
|
```
|
|
134
177
|
|
|
@@ -157,7 +200,7 @@ Handlers fire in reverse registration order (innermost component first). Call `s
|
|
|
157
200
|
Declarative key binding. Parses `'ctrl+s'`, `'alt+enter'`, etc.
|
|
158
201
|
|
|
159
202
|
```jsx
|
|
160
|
-
import { useHotkey } from '
|
|
203
|
+
import { useHotkey } from '@trendr/core'
|
|
161
204
|
|
|
162
205
|
useHotkey('ctrl+s', () => save())
|
|
163
206
|
useHotkey('alt+enter', () => submit(), { when: () => isFocused })
|
|
@@ -185,12 +228,73 @@ Used in [dashboard](examples/dashboard.jsx)
|
|
|
185
228
|
useInterval(() => tick(), 1000) // auto-cleaned on unmount
|
|
186
229
|
```
|
|
187
230
|
|
|
231
|
+
### useTimeout
|
|
232
|
+
|
|
233
|
+
Used in [timeout](examples/timeout.jsx)
|
|
234
|
+
|
|
235
|
+
Single-shot timer. Auto-cleaned on unmount.
|
|
236
|
+
|
|
237
|
+
```jsx
|
|
238
|
+
useTimeout(() => hide(), 3000)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### useAsync
|
|
242
|
+
|
|
243
|
+
Used in [async](examples/async.jsx)
|
|
244
|
+
|
|
245
|
+
Async function to reactive signals.
|
|
246
|
+
|
|
247
|
+
```jsx
|
|
248
|
+
import { useAsync } from '@trendr/core'
|
|
249
|
+
|
|
250
|
+
const { status, data, error, run } = useAsync(fetchUsers)
|
|
251
|
+
|
|
252
|
+
// status(): 'idle' | 'loading' | 'success' | 'error'
|
|
253
|
+
// data(): resolved value (null until success)
|
|
254
|
+
// error(): rejected error (null until error)
|
|
255
|
+
// run(): trigger the async function. forwards args: run(userId)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Stale calls are discarded. Use `{ immediate: true }` to fire on mount:
|
|
259
|
+
|
|
260
|
+
```jsx
|
|
261
|
+
const { status, data } = useAsync(fetchUsers, { immediate: true })
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### useMouse
|
|
265
|
+
|
|
266
|
+
```jsx
|
|
267
|
+
useMouse((event) => {
|
|
268
|
+
// event.action: 'press' | 'release' | 'drag' | 'scroll'
|
|
269
|
+
// event.button: 'left' | 'middle' | 'right' (press/release only)
|
|
270
|
+
// event.direction: 'up' | 'down' (scroll only)
|
|
271
|
+
// event.x, event.y: 0-based terminal coordinates
|
|
272
|
+
// event.stopPropagation(): prevent other handlers from receiving this event
|
|
273
|
+
})
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Mouse is enabled automatically. Built-in components support click, scroll wheel, and scrollbar dragging.
|
|
277
|
+
|
|
188
278
|
### useStdout
|
|
189
279
|
|
|
190
280
|
```jsx
|
|
191
281
|
const stream = useStdout() // the output stream (process.stdout or custom)
|
|
192
282
|
```
|
|
193
283
|
|
|
284
|
+
### useRepaint
|
|
285
|
+
|
|
286
|
+
Forces a full repaint. Useful after spawning an external process (e.g. `$EDITOR`).
|
|
287
|
+
|
|
288
|
+
```jsx
|
|
289
|
+
const repaint = useRepaint()
|
|
290
|
+
|
|
291
|
+
// leave alt screen, spawn editor, re-enter, repaint
|
|
292
|
+
stdout.write('\x1b[?1049l\x1b[?25h')
|
|
293
|
+
execSync(`${process.env.EDITOR} ${file}`, { stdio: 'inherit' })
|
|
294
|
+
stdout.write('\x1b[?1049h\x1b[?25l')
|
|
295
|
+
repaint()
|
|
296
|
+
```
|
|
297
|
+
|
|
194
298
|
### useTheme
|
|
195
299
|
|
|
196
300
|
Returns the current theme object. See [Theming](#theming).
|
|
@@ -203,10 +307,10 @@ const { accent } = useTheme()
|
|
|
203
307
|
|
|
204
308
|
Used in [explorer](examples/explorer.jsx), [chat](examples/chat.jsx), [modal-form](examples/modal-form.jsx), [components](examples/components.jsx), [focus-demo](examples/focus-demo.jsx), [layout](examples/layout.jsx)
|
|
205
309
|
|
|
206
|
-
|
|
310
|
+
Register named items in tab order. The focus manager tracks which is active.
|
|
207
311
|
|
|
208
312
|
```jsx
|
|
209
|
-
import { useFocus } from '
|
|
313
|
+
import { useFocus } from '@trendr/core'
|
|
210
314
|
|
|
211
315
|
const fm = useFocus({ initial: 'input' })
|
|
212
316
|
|
|
@@ -216,30 +320,29 @@ fm.item('list') // tab stop 1
|
|
|
216
320
|
fm.item('sidebar') // tab stop 2
|
|
217
321
|
```
|
|
218
322
|
|
|
219
|
-
|
|
323
|
+
Wire `fm.is()` to each component's `focused` prop. Tab/shift-tab cycles through items.
|
|
220
324
|
|
|
221
325
|
```jsx
|
|
222
326
|
<TextInput focused={fm.is('input')} />
|
|
223
327
|
<List focused={fm.is('list')} />
|
|
224
328
|
<Select focused={fm.is('sidebar')} />
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
Tab and shift-tab cycle through the registered names. The focus manager handles the index - you just query it.
|
|
228
329
|
|
|
229
|
-
|
|
230
|
-
fm.
|
|
231
|
-
fm.focus('list') // jump to 'list' programmatically
|
|
232
|
-
fm.current() // the currently active name
|
|
330
|
+
fm.focus('list') // jump programmatically
|
|
331
|
+
fm.current() // the active name
|
|
233
332
|
```
|
|
234
333
|
|
|
235
|
-
Groups
|
|
334
|
+
Groups nest multiple items under one tab stop:
|
|
236
335
|
|
|
237
336
|
```jsx
|
|
238
|
-
fm.group('settings', { items: ['theme', 'autosave', 'format']
|
|
337
|
+
fm.group('settings', { items: ['theme', 'autosave', 'format'] })
|
|
239
338
|
// fm.is('theme'), fm.is('autosave'), etc. work within the group
|
|
240
339
|
```
|
|
241
340
|
|
|
242
|
-
|
|
341
|
+
Options:
|
|
342
|
+
- `navigate` - which keys move between group items: `'both'` (default, j/k and up/down), `'jk'`, or `'updown'`
|
|
343
|
+
- `wrap` - wrap around at ends (default `false`)
|
|
344
|
+
|
|
345
|
+
Stack-based focus for modals - push saves current focus, pop restores it:
|
|
243
346
|
|
|
244
347
|
```jsx
|
|
245
348
|
fm.push('modal') // save current focus, switch to 'modal'
|
|
@@ -251,12 +354,17 @@ fm.pop() // restore previous focus
|
|
|
251
354
|
Used in [chat](examples/chat.jsx), [modal-form](examples/modal-form.jsx), [components](examples/components.jsx)
|
|
252
355
|
|
|
253
356
|
```jsx
|
|
254
|
-
import { useToast } from '
|
|
357
|
+
import { useToast } from '@trendr/core'
|
|
255
358
|
|
|
256
359
|
const toast = useToast({
|
|
257
360
|
duration: 2000, // ms, default 2000
|
|
258
361
|
position: 'bottom-right', // see positions below
|
|
259
362
|
margin: 1, // padding from screen edge, default 1
|
|
363
|
+
render: (message) => ( // optional custom render
|
|
364
|
+
<box style={{ bg: '#1E1E1E', paddingX: 1 }}>
|
|
365
|
+
<text style={{ color: '#9A9EA3' }}>{message}</text>
|
|
366
|
+
</box>
|
|
367
|
+
),
|
|
260
368
|
})
|
|
261
369
|
|
|
262
370
|
toast('saved')
|
|
@@ -268,7 +376,7 @@ toast('saved')
|
|
|
268
376
|
|
|
269
377
|
## Components
|
|
270
378
|
|
|
271
|
-
All interactive components accept a `focused` prop
|
|
379
|
+
All interactive components accept a `focused` prop. Wire it to a focus manager so only one component captures keys at a time:
|
|
272
380
|
|
|
273
381
|
```jsx
|
|
274
382
|
const fm = useFocus({ initial: 'search' })
|
|
@@ -328,19 +436,36 @@ Scrollable list with keyboard navigation.
|
|
|
328
436
|
selected={selectedIndex} // controlled, or omit for internal state
|
|
329
437
|
onSelect={setIndex}
|
|
330
438
|
focused={fm.is('list')}
|
|
331
|
-
|
|
439
|
+
scrollbar={true} // default false
|
|
332
440
|
header={<text>title</text>}
|
|
441
|
+
headerHeight={1} // default 1, rows the header occupies
|
|
333
442
|
renderItem={(item, { selected, index, focused }) => (
|
|
334
443
|
<text style={{ bg: selected ? (focused ? accent : 'gray') : null }}>{item.name}</text>
|
|
335
444
|
)}
|
|
336
445
|
/>
|
|
337
446
|
```
|
|
338
447
|
|
|
448
|
+
`itemHeight` enables multi-row items (tells scroll math how many rows each item occupies):
|
|
449
|
+
|
|
450
|
+
```jsx
|
|
451
|
+
<List
|
|
452
|
+
items={data}
|
|
453
|
+
itemHeight={3}
|
|
454
|
+
renderItem={(item, { selected, focused }) => (
|
|
455
|
+
<box style={{ flexDirection: 'column', bg: selected ? accent : null }}>
|
|
456
|
+
<text style={{ bold: true }}>{item.name}</text>
|
|
457
|
+
<text style={{ color: 'gray' }}>{item.description}</text>
|
|
458
|
+
<text style={{ color: 'green' }}>{item.status}</text>
|
|
459
|
+
</box>
|
|
460
|
+
)}
|
|
461
|
+
/>
|
|
462
|
+
```
|
|
463
|
+
|
|
339
464
|
Keys: j/k or up/down, g/G for top/bottom, ctrl-d/u half page, ctrl-f/b full page, pageup/pagedown.
|
|
340
465
|
|
|
341
466
|
### Table
|
|
342
467
|
|
|
343
|
-
Used in [components](examples/components.jsx)
|
|
468
|
+
Used in [components](examples/components.jsx), [custom-table](examples/custom-table.jsx)
|
|
344
469
|
|
|
345
470
|
Column-based data table. Uses List internally.
|
|
346
471
|
|
|
@@ -355,6 +480,25 @@ Column-based data table. Uses List internally.
|
|
|
355
480
|
selected={selectedRow}
|
|
356
481
|
onSelect={setRow}
|
|
357
482
|
focused={fm.is('table')}
|
|
483
|
+
separator={true} // horizontal rule below header
|
|
484
|
+
separatorChars={{ left: '', fill: '─', right: '' }} // customizable
|
|
485
|
+
/>
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
`renderItem` gives full control over row rendering while keeping column-aligned headers:
|
|
489
|
+
|
|
490
|
+
```jsx
|
|
491
|
+
<Table
|
|
492
|
+
columns={columns}
|
|
493
|
+
data={rows}
|
|
494
|
+
selected={idx()}
|
|
495
|
+
onSelect={setIdx}
|
|
496
|
+
renderItem={(row, { selected, focused }) => (
|
|
497
|
+
<box style={{ flexDirection: 'row', bg: selected ? accent : null, paddingX: 1 }}>
|
|
498
|
+
<text style={{ color: selected ? 'black' : null, flexGrow: 1 }}>{row.name}</text>
|
|
499
|
+
<text style={{ color: selected ? 'black' : row.stale ? 'yellow' : 'gray' }}>{row.age}</text>
|
|
500
|
+
</box>
|
|
501
|
+
)}
|
|
358
502
|
/>
|
|
359
503
|
```
|
|
360
504
|
|
|
@@ -387,6 +531,8 @@ Dropdown selector. Can render inline or as overlay.
|
|
|
387
531
|
focused={fm.is('color')}
|
|
388
532
|
overlay={false} // true renders as floating overlay
|
|
389
533
|
placeholder="pick one..."
|
|
534
|
+
openIcon="▲" // default ▲
|
|
535
|
+
closedIcon="▼" // default ▼
|
|
390
536
|
renderItem={(item, { selected, index }) => <text>{item}</text>}
|
|
391
537
|
style={{
|
|
392
538
|
border: 'single', borderColor: 'green', bg: 'black',
|
|
@@ -408,10 +554,12 @@ Used in [modal-form](examples/modal-form.jsx), [components](examples/components.
|
|
|
408
554
|
label="Enable feature"
|
|
409
555
|
onChange={setChecked} // (newState: boolean) => void
|
|
410
556
|
focused={fm.is('feature')}
|
|
557
|
+
checkedIcon="[✓]" // default '[x]'
|
|
558
|
+
uncheckedIcon="[ ]" // default '[ ]'
|
|
411
559
|
/>
|
|
412
560
|
```
|
|
413
561
|
|
|
414
|
-
Keys: space or enter to toggle.
|
|
562
|
+
Keys: space or enter to toggle.
|
|
415
563
|
|
|
416
564
|
### Radio
|
|
417
565
|
|
|
@@ -426,21 +574,34 @@ Used in [modal-form](examples/modal-form.jsx), [components](examples/components.
|
|
|
426
574
|
/>
|
|
427
575
|
```
|
|
428
576
|
|
|
429
|
-
Keys: j/k or up/down, enter/space to select. Renders
|
|
577
|
+
Keys: j/k or up/down, enter/space to select. Renders `●` / `○`.
|
|
430
578
|
|
|
431
579
|
### ProgressBar
|
|
432
580
|
|
|
433
|
-
Used in [
|
|
581
|
+
Used in [progress](examples/progress.jsx), [components](examples/components.jsx)
|
|
434
582
|
|
|
435
583
|
```jsx
|
|
436
584
|
<ProgressBar
|
|
437
|
-
value={0.65}
|
|
438
|
-
|
|
439
|
-
color="red"
|
|
440
|
-
label="
|
|
585
|
+
value={0.65} // 0 to 1
|
|
586
|
+
variant="thin" // 'thin' (default), 'block', 'ascii', 'braille'
|
|
587
|
+
color="red" // overrides theme accent
|
|
588
|
+
label="Installing" // optional label before bar
|
|
589
|
+
count="8/12" // optional count after percentage
|
|
590
|
+
percentage={true} // show percentage (default true)
|
|
591
|
+
width={30} // override bar width (default: fills available space)
|
|
441
592
|
/>
|
|
442
593
|
```
|
|
443
594
|
|
|
595
|
+
Variants:
|
|
596
|
+
- `thin` - clean `━` bar (default)
|
|
597
|
+
- `block` - thick `█░` blocks
|
|
598
|
+
- `ascii` - plain `[###---]`, works in any terminal
|
|
599
|
+
- `braille` - smooth `⣿` fill
|
|
600
|
+
|
|
601
|
+
```
|
|
602
|
+
Installing ━━━━━━━━━━━━━━━━━━━━━━━━ 67% (8/12)
|
|
603
|
+
```
|
|
604
|
+
|
|
444
605
|
### Spinner
|
|
445
606
|
|
|
446
607
|
Used in [components](examples/components.jsx)
|
|
@@ -448,11 +609,59 @@ Used in [components](examples/components.jsx)
|
|
|
448
609
|
```jsx
|
|
449
610
|
<Spinner
|
|
450
611
|
label="loading..."
|
|
612
|
+
variant="dots" // 'dots' (default), 'line', 'circle', 'bounce', 'arrow', 'square', 'star'
|
|
451
613
|
color="magenta" // overrides theme accent
|
|
452
614
|
interval={80} // ms, default 80
|
|
615
|
+
frames={['a','b']} // custom frames (overrides variant)
|
|
616
|
+
/>
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Task
|
|
620
|
+
|
|
621
|
+
Used in [task](examples/task.jsx)
|
|
622
|
+
|
|
623
|
+
Spinner while loading, checkmark on success, x on error. Built on `useAsync`.
|
|
624
|
+
|
|
625
|
+
```jsx
|
|
626
|
+
<Task
|
|
627
|
+
run={() => fetchData()} // async function
|
|
628
|
+
label="Fetching data..." // shown while loading
|
|
629
|
+
successLabel="Done" // optional, shown on success (defaults to label)
|
|
630
|
+
errorLabel="Failed" // optional, shown on error (defaults to error message)
|
|
631
|
+
immediate={true} // fire on mount (default true)
|
|
632
|
+
icon={{ success: '+' }} // override icons per status
|
|
633
|
+
color="cyan" // override color (defaults vary by status)
|
|
453
634
|
/>
|
|
454
635
|
```
|
|
455
636
|
|
|
637
|
+
Multiple tasks render as a step list:
|
|
638
|
+
|
|
639
|
+
```jsx
|
|
640
|
+
<Task run={() => install()} label="Installing..." successLabel="Installed" />
|
|
641
|
+
<Task run={() => build()} label="Building..." successLabel="Built" />
|
|
642
|
+
<Task run={() => test()} label="Testing..." successLabel="Tests passed" />
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Shimmer
|
|
646
|
+
|
|
647
|
+
Used in [shimmer](examples/shimmer.jsx)
|
|
648
|
+
|
|
649
|
+
Sliding highlight effect with gradient falloff.
|
|
650
|
+
|
|
651
|
+
```jsx
|
|
652
|
+
<Shimmer
|
|
653
|
+
color="gray" // base text color (default 'gray')
|
|
654
|
+
highlight="cyan" // shimmer color (default: theme accent)
|
|
655
|
+
size={3} // width of bright center in chars (default 3)
|
|
656
|
+
gradient={3} // gradient tail length each side (default 3, 0 for hard edge)
|
|
657
|
+
duration={1000} // ms for one pass across the text (default 1000)
|
|
658
|
+
delay={500} // ms pause between passes (default 500)
|
|
659
|
+
reverse={false} // slide right to left (default false)
|
|
660
|
+
>
|
|
661
|
+
Loading resources...
|
|
662
|
+
</Shimmer>
|
|
663
|
+
```
|
|
664
|
+
|
|
456
665
|
### Button
|
|
457
666
|
|
|
458
667
|
Used in [modal-form](examples/modal-form.jsx)
|
|
@@ -492,7 +701,7 @@ Keys: escape to close.
|
|
|
492
701
|
|
|
493
702
|
Used in [explorer](examples/explorer.jsx), [reader](examples/reader.jsx), [highlight](examples/highlight.jsx)
|
|
494
703
|
|
|
495
|
-
Scrollable text viewer
|
|
704
|
+
Scrollable text viewer. ANSI escape sequences are parsed and rendered, so syntax highlighter output (shiki, cli-highlight, etc.) works directly.
|
|
496
705
|
|
|
497
706
|
```jsx
|
|
498
707
|
<ScrollableText
|
|
@@ -501,12 +710,117 @@ Scrollable text viewer with optional scrollbar. Content can include ANSI escape
|
|
|
501
710
|
scrollOffset={offset} // controlled, or omit for internal state
|
|
502
711
|
onScroll={setOffset}
|
|
503
712
|
scrollbar={true} // default false
|
|
504
|
-
wrap={false} // default true,
|
|
713
|
+
wrap={false} // default true, false truncates long lines
|
|
714
|
+
thumbChar="█" // default █
|
|
715
|
+
trackChar="│" // default │
|
|
505
716
|
/>
|
|
506
717
|
```
|
|
507
718
|
|
|
508
719
|
Keys: same as List (j/k, g/G, ctrl-d/u, ctrl-f/b, pageup/pagedown).
|
|
509
720
|
|
|
721
|
+
### ScrollBox
|
|
722
|
+
|
|
723
|
+
Scrollable container for JSX children (vs ScrollableText which takes a string).
|
|
724
|
+
|
|
725
|
+
```jsx
|
|
726
|
+
<ScrollBox
|
|
727
|
+
focused={fm.is('list')}
|
|
728
|
+
scrollbar={true} // default false
|
|
729
|
+
scrollOffset={offset} // controlled, or omit for internal state
|
|
730
|
+
onScroll={setOffset}
|
|
731
|
+
thumbChar="█" // default █
|
|
732
|
+
trackChar="│" // default │
|
|
733
|
+
style={{ flexGrow: 1 }} // pass-through style for the scroll container
|
|
734
|
+
>
|
|
735
|
+
{items.map(item => (
|
|
736
|
+
<text key={item.id}>{item.name}</text>
|
|
737
|
+
))}
|
|
738
|
+
</ScrollBox>
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
Keys: same as List and ScrollableText.
|
|
742
|
+
|
|
743
|
+
### SplitPane
|
|
744
|
+
|
|
745
|
+
Paneled layout with shared borders and junction characters. Sizes use `fr` units or fixed values.
|
|
746
|
+
|
|
747
|
+
```jsx
|
|
748
|
+
import { SplitPane } from '@trendr/core'
|
|
749
|
+
|
|
750
|
+
<SplitPane direction="row" sizes={[20, '2fr', '1fr']} border="round" borderColor="gray">
|
|
751
|
+
<box style={{ paddingX: 1 }}>
|
|
752
|
+
<text>sidebar</text>
|
|
753
|
+
</box>
|
|
754
|
+
<box style={{ paddingX: 1 }}>
|
|
755
|
+
<text>main content</text>
|
|
756
|
+
</box>
|
|
757
|
+
<box style={{ paddingX: 1 }}>
|
|
758
|
+
<text>detail</text>
|
|
759
|
+
</box>
|
|
760
|
+
</SplitPane>
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
Props:
|
|
764
|
+
- `direction` - `'row'` (vertical dividers) or `'column'` (horizontal dividers)
|
|
765
|
+
- `sizes` - array of fixed numbers or `'Nfr'` strings. `[20, '1fr']` = 20 cols fixed + rest. `['1fr', '1fr']` = even split. Defaults to equal fractions.
|
|
766
|
+
- `border` - `'single'` | `'double'` | `'round'` | `'bold'`
|
|
767
|
+
- `borderColor` - color for border and dividers
|
|
768
|
+
- `borderEdges` - object with `top`, `right`, `bottom`, `left` booleans to render only specific sides. Omitted keys default to false.
|
|
769
|
+
|
|
770
|
+
Nesting works:
|
|
771
|
+
|
|
772
|
+
```jsx
|
|
773
|
+
<SplitPane direction="column" sizes={['1fr', 8]} border="round">
|
|
774
|
+
<SplitPane direction="row" sizes={[20, '1fr']} border="round">
|
|
775
|
+
<box>nav</box>
|
|
776
|
+
<box>main</box>
|
|
777
|
+
</SplitPane>
|
|
778
|
+
<box>status</box>
|
|
779
|
+
</SplitPane>
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
## Animation
|
|
783
|
+
|
|
784
|
+
Physics-based animation. Animated values are signals that trigger re-renders.
|
|
785
|
+
|
|
786
|
+
```jsx
|
|
787
|
+
import { useAnimated, spring, ease, decay } from '@trendr/core'
|
|
788
|
+
|
|
789
|
+
const x = useAnimated(0, spring()) // spring physics
|
|
790
|
+
x.set(100) // animate to 100
|
|
791
|
+
x() // read current value (tracks as signal)
|
|
792
|
+
x.snap(50) // jump instantly, no animation
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
`useAnimated` is the hook version (auto-cleanup on unmount). `animated` is the standalone version for use outside components.
|
|
796
|
+
|
|
797
|
+
### Interpolators
|
|
798
|
+
|
|
799
|
+
```js
|
|
800
|
+
spring({ frequency: 2, damping: 0.3 }) // underdamped spring (bouncy)
|
|
801
|
+
spring({ damping: 1 }) // critically damped (no bounce)
|
|
802
|
+
ease(300) // 300ms ease-out-cubic
|
|
803
|
+
ease(500, linear) // 500ms linear
|
|
804
|
+
decay({ deceleration: 0.998 }) // momentum-based decay
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
Switch interpolator mid-animation:
|
|
808
|
+
|
|
809
|
+
```js
|
|
810
|
+
x.setInterpolator(ease(200))
|
|
811
|
+
x.set(newTarget)
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
### Easing functions
|
|
815
|
+
|
|
816
|
+
`linear`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeOutElastic()`, `easeOutBounce()`
|
|
817
|
+
|
|
818
|
+
### Tick callback
|
|
819
|
+
|
|
820
|
+
```js
|
|
821
|
+
x.onTick((value) => { /* called each frame while animating */ })
|
|
822
|
+
```
|
|
823
|
+
|
|
510
824
|
## Build
|
|
511
825
|
|
|
512
826
|
Uses esbuild. JSX configured with `jsxImportSource: 'trend'`.
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { mount } from './src/renderer.js'
|
|
2
2
|
export { createSignal } from './src/signal.js'
|
|
3
3
|
export { createEffect, createMemo, batch, untrack, onCleanup } from './src/signal.js'
|
|
4
|
-
export { useInput, useResize, useInterval, useLayout, useStdout, useTitle, useTheme } from './src/hooks.js'
|
|
4
|
+
export { useState, useInput, useMouse, useResize, useInterval, useTimeout, useLayout, useStdout, useRepaint, useTitle, useTheme, useCursor, useFrameStats, useAsync } from './src/hooks.js'
|
|
5
5
|
export { Box, Text, Spacer } from './src/components.js'
|
|
6
6
|
export { TextInput } from './src/text-input.js'
|
|
7
7
|
export { TextArea } from './src/text-area.js'
|
|
@@ -16,7 +16,13 @@ export { Modal } from './src/modal.js'
|
|
|
16
16
|
export { Radio } from './src/radio.js'
|
|
17
17
|
export { Button } from './src/button.js'
|
|
18
18
|
export { ScrollableText } from './src/scrollable-text.js'
|
|
19
|
+
export { ScrollBox } from './src/scroll-box.js'
|
|
20
|
+
export { SplitPane } from './src/split-pane.js'
|
|
21
|
+
export { Task } from './src/task.js'
|
|
22
|
+
export { Shimmer } from './src/shimmer.js'
|
|
19
23
|
export { useFocus } from './src/focus.js'
|
|
20
24
|
export { useHotkey } from './src/hotkey.js'
|
|
21
25
|
export { useToast } from './src/toast.js'
|
|
22
26
|
export { Fragment } from './src/element.js'
|
|
27
|
+
export { animated, useAnimated, spring, ease, decay } from './src/animation.js'
|
|
28
|
+
export { linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeOutElastic, easeOutBounce } from './src/animation.js'
|