@trendr/core 0.1.0 → 0.2.1
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 +420 -56
- package/index.js +9 -1
- package/package.json +23 -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 +142 -17
- package/src/modal.js +2 -2
- package/src/pick-list.js +204 -0
- package/src/progress.js +57 -9
- package/src/radio.js +16 -2
- package/src/renderer.js +364 -68
- 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 +68 -23
- 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,31 @@
|
|
|
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](https://github.com/facebook/yoga/issues/1859). 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/6e84bb4b-a99e-46f2-a235-1ee4be62c0ae
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
## Usage
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
```bash
|
|
18
|
+
npm i @trendr/core
|
|
19
|
+
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Requires esbuild (or similar) for JSX transformation.
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{ "jsx": "automatic", "jsxImportSource": "trend" }
|
|
25
|
+
```
|
|
22
26
|
|
|
23
27
|
```jsx
|
|
24
|
-
import { mount, createSignal, useInput } from '
|
|
28
|
+
import { mount, createSignal, useInput } from '@trendr/core'
|
|
25
29
|
|
|
26
30
|
function App() {
|
|
27
31
|
const [count, setCount] = createSignal(0)
|
|
@@ -42,20 +46,31 @@ function App() {
|
|
|
42
46
|
mount(App)
|
|
43
47
|
```
|
|
44
48
|
|
|
45
|
-
`mount(Component, { stream?, stdin?, theme? })` enters alt screen
|
|
49
|
+
`mount(Component, { stream?, stdin?, title?, theme? })` enters alt screen and returns `{ unmount, repaint }`. Renders on demand when signals change, capped at 60fps.
|
|
46
50
|
|
|
47
51
|
### Theming
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
Pass a theme object to `mount` to configure global defaults:
|
|
50
54
|
|
|
51
55
|
```jsx
|
|
52
|
-
mount(App, {
|
|
56
|
+
mount(App, {
|
|
57
|
+
theme: {
|
|
58
|
+
accent: 'green', // focus/highlight color, default 'cyan'
|
|
59
|
+
cursor: {
|
|
60
|
+
blink: true, // default false
|
|
61
|
+
rate: 530, // blink interval ms, default 530
|
|
62
|
+
style: 'block', // default 'block'
|
|
63
|
+
bg: 'cyan', // cursor background color
|
|
64
|
+
color: 'black', // cursor text color
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
})
|
|
53
68
|
```
|
|
54
69
|
|
|
55
|
-
Components read the
|
|
70
|
+
Components read the theme with `useTheme()`:
|
|
56
71
|
|
|
57
72
|
```jsx
|
|
58
|
-
import { useTheme } from '
|
|
73
|
+
import { useTheme } from '@trendr/core'
|
|
59
74
|
|
|
60
75
|
const { accent } = useTheme()
|
|
61
76
|
```
|
|
@@ -65,7 +80,7 @@ Individual components still accept explicit color props (e.g. `<Spinner color="m
|
|
|
65
80
|
## Signals
|
|
66
81
|
|
|
67
82
|
```js
|
|
68
|
-
import { createSignal, createEffect, createMemo, batch, untrack, onCleanup } from '
|
|
83
|
+
import { createSignal, createEffect, createMemo, batch, untrack, onCleanup } from '@trendr/core'
|
|
69
84
|
|
|
70
85
|
const [value, setValue] = createSignal(0)
|
|
71
86
|
value() // read (tracks dependency)
|
|
@@ -109,26 +124,58 @@ Two element types: `box` (container) and `text` (leaf).
|
|
|
109
124
|
margin: 1, // same variants as padding
|
|
110
125
|
border: 'round', // 'single' | 'double' | 'round' | 'bold'
|
|
111
126
|
borderColor: 'cyan',
|
|
127
|
+
borderEdges: { bottom: true, left: true }, // render only specific sides
|
|
112
128
|
bg: 'blue', // background color
|
|
129
|
+
texture: 'dots', // background texture (see below)
|
|
130
|
+
textureColor: '#333', // color for texture characters
|
|
131
|
+
position: 'absolute', // remove from flow, position with top/left/right/bottom
|
|
132
|
+
top: 0, left: 0, right: 0, bottom: 0,
|
|
133
|
+
overflow: 'scroll', // scrollable container (see ScrollBox)
|
|
134
|
+
scrollOffset: 0, // scroll position (rows from top)
|
|
113
135
|
}}>
|
|
136
|
+
```
|
|
114
137
|
|
|
138
|
+
```jsx
|
|
115
139
|
<text style={{
|
|
116
140
|
color: 'cyan', // named, hex (#ff0000), or 256-color index
|
|
117
141
|
bg: 'black',
|
|
118
|
-
bold: true,
|
|
119
|
-
|
|
120
|
-
italic: true,
|
|
121
|
-
underline: true,
|
|
122
|
-
inverse: true,
|
|
123
|
-
strikethrough: true,
|
|
142
|
+
bold: true, dim: true, italic: true,
|
|
143
|
+
underline: true, inverse: true, strikethrough: true,
|
|
124
144
|
overflow: 'wrap', // 'wrap' (default) | 'truncate' | 'nowrap'
|
|
125
145
|
}}>
|
|
126
146
|
```
|
|
127
147
|
|
|
148
|
+
### Background Textures
|
|
149
|
+
|
|
150
|
+
Repeating character fill for box backgrounds. Works with or without `bg`.
|
|
151
|
+
|
|
152
|
+
```jsx
|
|
153
|
+
<box style={{ bg: '#1a1a2e', texture: 'dots', textureColor: '#2a2a4e' }}>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Presets: `'shade-light'` (░), `'shade-medium'` (▒), `'shade-heavy'` (▓), `'dots'` (·), `'cross'` (╳), `'grid'` (┼), `'dash'` (╌). Or pass any single character: `texture: '~'`.
|
|
157
|
+
|
|
158
|
+
Texture characters show through spaces in text rendered on top (unless the text has an explicit `bg`, which claims the cell).
|
|
159
|
+
|
|
160
|
+
### Absolute Positioning
|
|
161
|
+
|
|
162
|
+
Position relative to parent, removed from flex flow.
|
|
163
|
+
|
|
164
|
+
```jsx
|
|
165
|
+
<box style={{ border: 'round', height: 5, flexDirection: 'column' }}>
|
|
166
|
+
<text>content here</text>
|
|
167
|
+
<box style={{ position: 'absolute', top: 0, right: 1 }}>
|
|
168
|
+
<text style={{ color: 'green', bold: true }}>ONLINE</text>
|
|
169
|
+
</box>
|
|
170
|
+
</box>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
If both `left` and `right` are set, width is derived (same for `top`/`bottom`).
|
|
174
|
+
|
|
128
175
|
`Box`, `Text`, and `Spacer` are convenience wrappers:
|
|
129
176
|
|
|
130
177
|
```jsx
|
|
131
|
-
import { Box, Text, Spacer } from '
|
|
178
|
+
import { Box, Text, Spacer } from '@trendr/core'
|
|
132
179
|
<Box style={{ flexDirection: 'row' }}><Text>hello</Text><Spacer /><Text>right</Text></Box>
|
|
133
180
|
```
|
|
134
181
|
|
|
@@ -157,7 +204,7 @@ Handlers fire in reverse registration order (innermost component first). Call `s
|
|
|
157
204
|
Declarative key binding. Parses `'ctrl+s'`, `'alt+enter'`, etc.
|
|
158
205
|
|
|
159
206
|
```jsx
|
|
160
|
-
import { useHotkey } from '
|
|
207
|
+
import { useHotkey } from '@trendr/core'
|
|
161
208
|
|
|
162
209
|
useHotkey('ctrl+s', () => save())
|
|
163
210
|
useHotkey('alt+enter', () => submit(), { when: () => isFocused })
|
|
@@ -165,10 +212,11 @@ useHotkey('alt+enter', () => submit(), { when: () => isFocused })
|
|
|
165
212
|
|
|
166
213
|
### useLayout
|
|
167
214
|
|
|
168
|
-
Returns the component's computed layout rectangle.
|
|
215
|
+
Returns a live reference to the component's computed layout rectangle. Values update in-place each frame, including after terminal resize.
|
|
169
216
|
|
|
170
217
|
```jsx
|
|
171
|
-
const
|
|
218
|
+
const layout = useLayout()
|
|
219
|
+
// layout.x, layout.y, layout.width, layout.height, layout.contentHeight
|
|
172
220
|
```
|
|
173
221
|
|
|
174
222
|
### useResize
|
|
@@ -185,12 +233,75 @@ Used in [dashboard](examples/dashboard.jsx)
|
|
|
185
233
|
useInterval(() => tick(), 1000) // auto-cleaned on unmount
|
|
186
234
|
```
|
|
187
235
|
|
|
236
|
+
### useTimeout
|
|
237
|
+
|
|
238
|
+
Used in [timeout](examples/timeout.jsx)
|
|
239
|
+
|
|
240
|
+
Single-shot timer. Auto-cleaned on unmount.
|
|
241
|
+
|
|
242
|
+
```jsx
|
|
243
|
+
useTimeout(() => hide(), 3000)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### useAsync
|
|
247
|
+
|
|
248
|
+
Used in [async](examples/async.jsx)
|
|
249
|
+
|
|
250
|
+
Async function to reactive signals.
|
|
251
|
+
|
|
252
|
+
```jsx
|
|
253
|
+
import { useAsync } from '@trendr/core'
|
|
254
|
+
|
|
255
|
+
const { status, data, error, run } = useAsync(fetchUsers)
|
|
256
|
+
|
|
257
|
+
// status(): 'idle' | 'loading' | 'success' | 'error'
|
|
258
|
+
// data(): resolved value (null until success)
|
|
259
|
+
// error(): rejected error (null until error)
|
|
260
|
+
// run(): trigger the async function. forwards args: run(userId)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Stale calls are discarded. Use `{ immediate: true }` to fire on mount:
|
|
264
|
+
|
|
265
|
+
```jsx
|
|
266
|
+
const { status, data } = useAsync(fetchUsers, { immediate: true })
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### useMouse
|
|
270
|
+
|
|
271
|
+
```jsx
|
|
272
|
+
useMouse((event) => {
|
|
273
|
+
// event.action: 'press' | 'release' | 'drag' | 'scroll'
|
|
274
|
+
// event.button: 'left' | 'middle' | 'right' (press/release only)
|
|
275
|
+
// event.direction: 'up' | 'down' (scroll only)
|
|
276
|
+
// event.x, event.y: 0-based terminal coordinates
|
|
277
|
+
// event.stopPropagation(): prevent other handlers from receiving this event
|
|
278
|
+
})
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Mouse is enabled automatically. Built-in components support click, scroll wheel, and scrollbar dragging.
|
|
282
|
+
|
|
188
283
|
### useStdout
|
|
189
284
|
|
|
190
285
|
```jsx
|
|
191
286
|
const stream = useStdout() // the output stream (process.stdout or custom)
|
|
192
287
|
```
|
|
193
288
|
|
|
289
|
+
### useRepaint
|
|
290
|
+
|
|
291
|
+
Forces a full repaint. Useful after spawning an external process (e.g. `$EDITOR`).
|
|
292
|
+
|
|
293
|
+
```jsx
|
|
294
|
+
import { useRepaint, useStdout, exitAltScreen, showCursor, altScreen, hideCursor } from '@trendr/core'
|
|
295
|
+
|
|
296
|
+
const repaint = useRepaint()
|
|
297
|
+
const stdout = useStdout()
|
|
298
|
+
|
|
299
|
+
stdout.write(exitAltScreen + showCursor)
|
|
300
|
+
execSync(`${process.env.EDITOR} ${file}`, { stdio: 'inherit' })
|
|
301
|
+
stdout.write(altScreen + hideCursor)
|
|
302
|
+
repaint()
|
|
303
|
+
```
|
|
304
|
+
|
|
194
305
|
### useTheme
|
|
195
306
|
|
|
196
307
|
Returns the current theme object. See [Theming](#theming).
|
|
@@ -203,10 +314,10 @@ const { accent } = useTheme()
|
|
|
203
314
|
|
|
204
315
|
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
316
|
|
|
206
|
-
|
|
317
|
+
Register named items in tab order. The focus manager tracks which is active.
|
|
207
318
|
|
|
208
319
|
```jsx
|
|
209
|
-
import { useFocus } from '
|
|
320
|
+
import { useFocus } from '@trendr/core'
|
|
210
321
|
|
|
211
322
|
const fm = useFocus({ initial: 'input' })
|
|
212
323
|
|
|
@@ -216,30 +327,29 @@ fm.item('list') // tab stop 1
|
|
|
216
327
|
fm.item('sidebar') // tab stop 2
|
|
217
328
|
```
|
|
218
329
|
|
|
219
|
-
|
|
330
|
+
Wire `fm.is()` to each component's `focused` prop. Tab/shift-tab cycles through items.
|
|
220
331
|
|
|
221
332
|
```jsx
|
|
222
333
|
<TextInput focused={fm.is('input')} />
|
|
223
334
|
<List focused={fm.is('list')} />
|
|
224
335
|
<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
336
|
|
|
229
|
-
|
|
230
|
-
fm.
|
|
231
|
-
fm.focus('list') // jump to 'list' programmatically
|
|
232
|
-
fm.current() // the currently active name
|
|
337
|
+
fm.focus('list') // jump programmatically
|
|
338
|
+
fm.current() // the active name
|
|
233
339
|
```
|
|
234
340
|
|
|
235
|
-
Groups
|
|
341
|
+
Groups nest multiple items under one tab stop:
|
|
236
342
|
|
|
237
343
|
```jsx
|
|
238
|
-
fm.group('settings', { items: ['theme', 'autosave', 'format']
|
|
344
|
+
fm.group('settings', { items: ['theme', 'autosave', 'format'] })
|
|
239
345
|
// fm.is('theme'), fm.is('autosave'), etc. work within the group
|
|
240
346
|
```
|
|
241
347
|
|
|
242
|
-
|
|
348
|
+
Options:
|
|
349
|
+
- `navigate` - which keys move between group items: `'both'` (default, j/k and up/down), `'jk'`, or `'updown'`
|
|
350
|
+
- `wrap` - wrap around at ends (default `false`)
|
|
351
|
+
|
|
352
|
+
Stack-based focus for modals - push saves current focus, pop restores it:
|
|
243
353
|
|
|
244
354
|
```jsx
|
|
245
355
|
fm.push('modal') // save current focus, switch to 'modal'
|
|
@@ -251,12 +361,17 @@ fm.pop() // restore previous focus
|
|
|
251
361
|
Used in [chat](examples/chat.jsx), [modal-form](examples/modal-form.jsx), [components](examples/components.jsx)
|
|
252
362
|
|
|
253
363
|
```jsx
|
|
254
|
-
import { useToast } from '
|
|
364
|
+
import { useToast } from '@trendr/core'
|
|
255
365
|
|
|
256
366
|
const toast = useToast({
|
|
257
367
|
duration: 2000, // ms, default 2000
|
|
258
368
|
position: 'bottom-right', // see positions below
|
|
259
369
|
margin: 1, // padding from screen edge, default 1
|
|
370
|
+
render: (message) => ( // optional custom render
|
|
371
|
+
<box style={{ bg: '#1E1E1E', paddingX: 1 }}>
|
|
372
|
+
<text style={{ color: '#9A9EA3' }}>{message}</text>
|
|
373
|
+
</box>
|
|
374
|
+
),
|
|
260
375
|
})
|
|
261
376
|
|
|
262
377
|
toast('saved')
|
|
@@ -268,7 +383,7 @@ toast('saved')
|
|
|
268
383
|
|
|
269
384
|
## Components
|
|
270
385
|
|
|
271
|
-
All interactive components accept a `focused` prop
|
|
386
|
+
All interactive components accept a `focused` prop. Wire it to a focus manager so only one component captures keys at a time:
|
|
272
387
|
|
|
273
388
|
```jsx
|
|
274
389
|
const fm = useFocus({ initial: 'search' })
|
|
@@ -328,19 +443,79 @@ Scrollable list with keyboard navigation.
|
|
|
328
443
|
selected={selectedIndex} // controlled, or omit for internal state
|
|
329
444
|
onSelect={setIndex}
|
|
330
445
|
focused={fm.is('list')}
|
|
331
|
-
|
|
446
|
+
scrollbar={true} // default false
|
|
447
|
+
scrolloff={2} // items of margin from edges when scrolling (default 2)
|
|
448
|
+
interactive={true} // handle keyboard input (default: same as focused)
|
|
332
449
|
header={<text>title</text>}
|
|
450
|
+
headerHeight={1} // default 1, rows the header occupies
|
|
333
451
|
renderItem={(item, { selected, index, focused }) => (
|
|
334
452
|
<text style={{ bg: selected ? (focused ? accent : 'gray') : null }}>{item.name}</text>
|
|
335
453
|
)}
|
|
336
454
|
/>
|
|
337
455
|
```
|
|
338
456
|
|
|
457
|
+
`itemHeight` enables multi-row items (tells scroll math how many rows each item occupies):
|
|
458
|
+
|
|
459
|
+
```jsx
|
|
460
|
+
<List
|
|
461
|
+
items={data}
|
|
462
|
+
itemHeight={3}
|
|
463
|
+
renderItem={(item, { selected, focused }) => (
|
|
464
|
+
<box style={{ flexDirection: 'column', bg: selected ? accent : null }}>
|
|
465
|
+
<text style={{ bold: true }}>{item.name}</text>
|
|
466
|
+
<text style={{ color: 'gray' }}>{item.description}</text>
|
|
467
|
+
<text style={{ color: 'green' }}>{item.status}</text>
|
|
468
|
+
</box>
|
|
469
|
+
)}
|
|
470
|
+
/>
|
|
471
|
+
```
|
|
472
|
+
|
|
339
473
|
Keys: j/k or up/down, g/G for top/bottom, ctrl-d/u half page, ctrl-f/b full page, pageup/pagedown.
|
|
340
474
|
|
|
475
|
+
### PickList
|
|
476
|
+
|
|
477
|
+
Used in [pick-list](examples/pick-list.jsx)
|
|
478
|
+
|
|
479
|
+
Filterable list with live search. Text input at the top filters a scrollable list below. Navigate the list with up/down or ctrl-n/ctrl-p while typing.
|
|
480
|
+
|
|
481
|
+
```jsx
|
|
482
|
+
<PickList
|
|
483
|
+
items={data}
|
|
484
|
+
focused={fm.is('search')}
|
|
485
|
+
placeholder="search..."
|
|
486
|
+
onSelect={item => {}} // Enter on highlighted item
|
|
487
|
+
onCancel={() => {}} // Escape
|
|
488
|
+
onChange={query => {}} // every keystroke in the filter
|
|
489
|
+
clearOnSelect={false} // reset filter on select (default false)
|
|
490
|
+
scrollbar={true} // default false
|
|
491
|
+
scrolloff={2} // items of margin from edges (default 2, inherited from List)
|
|
492
|
+
gap={1} // space between input and list (default 0)
|
|
493
|
+
filter={(query, item) => {}} // custom filter (default: case-insensitive includes)
|
|
494
|
+
/>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Multi-row items with `renderItem`, `itemHeight`, and `itemGap`:
|
|
498
|
+
|
|
499
|
+
```jsx
|
|
500
|
+
<PickList
|
|
501
|
+
items={packages}
|
|
502
|
+
itemHeight={3}
|
|
503
|
+
itemGap={1}
|
|
504
|
+
renderItem={(pkg, { selected, focused }) => (
|
|
505
|
+
<box style={{ flexDirection: 'column', bg: selected ? accent : null, paddingX: 1 }}>
|
|
506
|
+
<text style={{ bold: true }}>{pkg.name}</text>
|
|
507
|
+
<text style={{ color: 'gray' }}>{pkg.desc}</text>
|
|
508
|
+
<text style={{ color: 'yellow' }}>{pkg.downloads}</text>
|
|
509
|
+
</box>
|
|
510
|
+
)}
|
|
511
|
+
/>
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Keys: type to filter, up/down or ctrl-n/ctrl-p to navigate, enter to select, escape to cancel. All bash-style editing keys work (ctrl-a/e/u/k/w).
|
|
515
|
+
|
|
341
516
|
### Table
|
|
342
517
|
|
|
343
|
-
Used in [components](examples/components.jsx)
|
|
518
|
+
Used in [components](examples/components.jsx), [custom-table](examples/custom-table.jsx)
|
|
344
519
|
|
|
345
520
|
Column-based data table. Uses List internally.
|
|
346
521
|
|
|
@@ -355,6 +530,25 @@ Column-based data table. Uses List internally.
|
|
|
355
530
|
selected={selectedRow}
|
|
356
531
|
onSelect={setRow}
|
|
357
532
|
focused={fm.is('table')}
|
|
533
|
+
separator={true} // horizontal rule below header
|
|
534
|
+
separatorChars={{ left: '', fill: '─', right: '' }} // customizable
|
|
535
|
+
/>
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
`renderItem` gives full control over row rendering while keeping column-aligned headers:
|
|
539
|
+
|
|
540
|
+
```jsx
|
|
541
|
+
<Table
|
|
542
|
+
columns={columns}
|
|
543
|
+
data={rows}
|
|
544
|
+
selected={idx()}
|
|
545
|
+
onSelect={setIdx}
|
|
546
|
+
renderItem={(row, { selected, focused }) => (
|
|
547
|
+
<box style={{ flexDirection: 'row', bg: selected ? accent : null, paddingX: 1 }}>
|
|
548
|
+
<text style={{ color: selected ? 'black' : null, flexGrow: 1 }}>{row.name}</text>
|
|
549
|
+
<text style={{ color: selected ? 'black' : row.stale ? 'yellow' : 'gray' }}>{row.age}</text>
|
|
550
|
+
</box>
|
|
551
|
+
)}
|
|
358
552
|
/>
|
|
359
553
|
```
|
|
360
554
|
|
|
@@ -387,6 +581,8 @@ Dropdown selector. Can render inline or as overlay.
|
|
|
387
581
|
focused={fm.is('color')}
|
|
388
582
|
overlay={false} // true renders as floating overlay
|
|
389
583
|
placeholder="pick one..."
|
|
584
|
+
openIcon="▲" // default ▲
|
|
585
|
+
closedIcon="▼" // default ▼
|
|
390
586
|
renderItem={(item, { selected, index }) => <text>{item}</text>}
|
|
391
587
|
style={{
|
|
392
588
|
border: 'single', borderColor: 'green', bg: 'black',
|
|
@@ -408,10 +604,12 @@ Used in [modal-form](examples/modal-form.jsx), [components](examples/components.
|
|
|
408
604
|
label="Enable feature"
|
|
409
605
|
onChange={setChecked} // (newState: boolean) => void
|
|
410
606
|
focused={fm.is('feature')}
|
|
607
|
+
checkedIcon="[✓]" // default '[x]'
|
|
608
|
+
uncheckedIcon="[ ]" // default '[ ]'
|
|
411
609
|
/>
|
|
412
610
|
```
|
|
413
611
|
|
|
414
|
-
Keys: space or enter to toggle.
|
|
612
|
+
Keys: space or enter to toggle.
|
|
415
613
|
|
|
416
614
|
### Radio
|
|
417
615
|
|
|
@@ -426,21 +624,34 @@ Used in [modal-form](examples/modal-form.jsx), [components](examples/components.
|
|
|
426
624
|
/>
|
|
427
625
|
```
|
|
428
626
|
|
|
429
|
-
Keys: j/k or up/down, enter/space to select. Renders
|
|
627
|
+
Keys: j/k or up/down, enter/space to select. Renders `●` / `○`.
|
|
430
628
|
|
|
431
629
|
### ProgressBar
|
|
432
630
|
|
|
433
|
-
Used in [
|
|
631
|
+
Used in [progress](examples/progress.jsx), [components](examples/components.jsx)
|
|
434
632
|
|
|
435
633
|
```jsx
|
|
436
634
|
<ProgressBar
|
|
437
|
-
value={0.65}
|
|
438
|
-
|
|
439
|
-
color="red"
|
|
440
|
-
label="
|
|
635
|
+
value={0.65} // 0 to 1
|
|
636
|
+
variant="thin" // 'thin' (default), 'block', 'ascii', 'braille'
|
|
637
|
+
color="red" // overrides theme accent
|
|
638
|
+
label="Installing" // optional label before bar
|
|
639
|
+
count="8/12" // optional count after percentage
|
|
640
|
+
percentage={true} // show percentage (default true)
|
|
641
|
+
width={30} // override bar width (default: fills available space)
|
|
441
642
|
/>
|
|
442
643
|
```
|
|
443
644
|
|
|
645
|
+
Variants:
|
|
646
|
+
- `thin` - clean `━` bar (default)
|
|
647
|
+
- `block` - thick `█░` blocks
|
|
648
|
+
- `ascii` - plain `[###---]`, works in any terminal
|
|
649
|
+
- `braille` - smooth `⣿` fill
|
|
650
|
+
|
|
651
|
+
```
|
|
652
|
+
Installing ━━━━━━━━━━━━━━━━━━━━━━━━ 67% (8/12)
|
|
653
|
+
```
|
|
654
|
+
|
|
444
655
|
### Spinner
|
|
445
656
|
|
|
446
657
|
Used in [components](examples/components.jsx)
|
|
@@ -448,11 +659,59 @@ Used in [components](examples/components.jsx)
|
|
|
448
659
|
```jsx
|
|
449
660
|
<Spinner
|
|
450
661
|
label="loading..."
|
|
662
|
+
variant="dots" // 'dots' (default), 'line', 'circle', 'bounce', 'arrow', 'square', 'star'
|
|
451
663
|
color="magenta" // overrides theme accent
|
|
452
664
|
interval={80} // ms, default 80
|
|
665
|
+
frames={['a','b']} // custom frames (overrides variant)
|
|
666
|
+
/>
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### Task
|
|
670
|
+
|
|
671
|
+
Used in [task](examples/task.jsx)
|
|
672
|
+
|
|
673
|
+
Spinner while loading, checkmark on success, x on error. Built on `useAsync`.
|
|
674
|
+
|
|
675
|
+
```jsx
|
|
676
|
+
<Task
|
|
677
|
+
run={() => fetchData()} // async function
|
|
678
|
+
label="Fetching data..." // shown while loading
|
|
679
|
+
successLabel="Done" // optional, shown on success (defaults to label)
|
|
680
|
+
errorLabel="Failed" // optional, shown on error (defaults to error message)
|
|
681
|
+
immediate={true} // fire on mount (default true)
|
|
682
|
+
icon={{ success: '+' }} // override icons per status
|
|
683
|
+
color="cyan" // override color (defaults vary by status)
|
|
453
684
|
/>
|
|
454
685
|
```
|
|
455
686
|
|
|
687
|
+
Multiple tasks render as a step list:
|
|
688
|
+
|
|
689
|
+
```jsx
|
|
690
|
+
<Task run={() => install()} label="Installing..." successLabel="Installed" />
|
|
691
|
+
<Task run={() => build()} label="Building..." successLabel="Built" />
|
|
692
|
+
<Task run={() => test()} label="Testing..." successLabel="Tests passed" />
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Shimmer
|
|
696
|
+
|
|
697
|
+
Used in [shimmer](examples/shimmer.jsx)
|
|
698
|
+
|
|
699
|
+
Sliding highlight effect with gradient falloff.
|
|
700
|
+
|
|
701
|
+
```jsx
|
|
702
|
+
<Shimmer
|
|
703
|
+
color="gray" // base text color (default 'gray')
|
|
704
|
+
highlight="cyan" // shimmer color (default: theme accent)
|
|
705
|
+
size={3} // width of bright center in chars (default 3)
|
|
706
|
+
gradient={3} // gradient tail length each side (default 3, 0 for hard edge)
|
|
707
|
+
duration={1000} // ms for one pass across the text (default 1000)
|
|
708
|
+
delay={500} // ms pause between passes (default 500)
|
|
709
|
+
reverse={false} // slide right to left (default false)
|
|
710
|
+
>
|
|
711
|
+
Loading resources...
|
|
712
|
+
</Shimmer>
|
|
713
|
+
```
|
|
714
|
+
|
|
456
715
|
### Button
|
|
457
716
|
|
|
458
717
|
Used in [modal-form](examples/modal-form.jsx)
|
|
@@ -492,7 +751,7 @@ Keys: escape to close.
|
|
|
492
751
|
|
|
493
752
|
Used in [explorer](examples/explorer.jsx), [reader](examples/reader.jsx), [highlight](examples/highlight.jsx)
|
|
494
753
|
|
|
495
|
-
Scrollable text viewer
|
|
754
|
+
Scrollable text viewer. ANSI escape sequences are parsed and rendered, so syntax highlighter output (shiki, cli-highlight, etc.) works directly.
|
|
496
755
|
|
|
497
756
|
```jsx
|
|
498
757
|
<ScrollableText
|
|
@@ -501,12 +760,117 @@ Scrollable text viewer with optional scrollbar. Content can include ANSI escape
|
|
|
501
760
|
scrollOffset={offset} // controlled, or omit for internal state
|
|
502
761
|
onScroll={setOffset}
|
|
503
762
|
scrollbar={true} // default false
|
|
504
|
-
wrap={false} // default true,
|
|
763
|
+
wrap={false} // default true, false truncates long lines
|
|
764
|
+
thumbChar="█" // default █
|
|
765
|
+
trackChar="│" // default │
|
|
505
766
|
/>
|
|
506
767
|
```
|
|
507
768
|
|
|
508
769
|
Keys: same as List (j/k, g/G, ctrl-d/u, ctrl-f/b, pageup/pagedown).
|
|
509
770
|
|
|
771
|
+
### ScrollBox
|
|
772
|
+
|
|
773
|
+
Scrollable container for JSX children (vs ScrollableText which takes a string).
|
|
774
|
+
|
|
775
|
+
```jsx
|
|
776
|
+
<ScrollBox
|
|
777
|
+
focused={fm.is('list')}
|
|
778
|
+
scrollbar={true} // default false
|
|
779
|
+
scrollOffset={offset} // controlled, or omit for internal state
|
|
780
|
+
onScroll={setOffset}
|
|
781
|
+
thumbChar="█" // default █
|
|
782
|
+
trackChar="│" // default │
|
|
783
|
+
style={{ flexGrow: 1 }} // pass-through style for the scroll container
|
|
784
|
+
>
|
|
785
|
+
{items.map(item => (
|
|
786
|
+
<text key={item.id}>{item.name}</text>
|
|
787
|
+
))}
|
|
788
|
+
</ScrollBox>
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
Keys: same as List and ScrollableText.
|
|
792
|
+
|
|
793
|
+
### SplitPane
|
|
794
|
+
|
|
795
|
+
Paneled layout with shared borders and junction characters. Sizes use `fr` units or fixed values.
|
|
796
|
+
|
|
797
|
+
```jsx
|
|
798
|
+
import { SplitPane } from '@trendr/core'
|
|
799
|
+
|
|
800
|
+
<SplitPane direction="row" sizes={[20, '2fr', '1fr']} border="round" borderColor="gray">
|
|
801
|
+
<box style={{ paddingX: 1 }}>
|
|
802
|
+
<text>sidebar</text>
|
|
803
|
+
</box>
|
|
804
|
+
<box style={{ paddingX: 1 }}>
|
|
805
|
+
<text>main content</text>
|
|
806
|
+
</box>
|
|
807
|
+
<box style={{ paddingX: 1 }}>
|
|
808
|
+
<text>detail</text>
|
|
809
|
+
</box>
|
|
810
|
+
</SplitPane>
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
Props:
|
|
814
|
+
- `direction` - `'row'` (vertical dividers) or `'column'` (horizontal dividers)
|
|
815
|
+
- `sizes` - array of fixed numbers or `'Nfr'` strings. `[20, '1fr']` = 20 cols fixed + rest. `['1fr', '1fr']` = even split. Defaults to equal fractions.
|
|
816
|
+
- `border` - `'single'` | `'double'` | `'round'` | `'bold'`
|
|
817
|
+
- `borderColor` - color for border and dividers
|
|
818
|
+
- `borderEdges` - object with `top`, `right`, `bottom`, `left` booleans to render only specific sides. Omitted keys default to false.
|
|
819
|
+
|
|
820
|
+
Nesting works:
|
|
821
|
+
|
|
822
|
+
```jsx
|
|
823
|
+
<SplitPane direction="column" sizes={['1fr', 8]} border="round">
|
|
824
|
+
<SplitPane direction="row" sizes={[20, '1fr']} border="round">
|
|
825
|
+
<box>nav</box>
|
|
826
|
+
<box>main</box>
|
|
827
|
+
</SplitPane>
|
|
828
|
+
<box>status</box>
|
|
829
|
+
</SplitPane>
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
## Animation
|
|
833
|
+
|
|
834
|
+
Physics-based animation. Animated values are signals that trigger re-renders.
|
|
835
|
+
|
|
836
|
+
```jsx
|
|
837
|
+
import { useAnimated, spring, ease, decay } from '@trendr/core'
|
|
838
|
+
|
|
839
|
+
const x = useAnimated(0, spring()) // spring physics
|
|
840
|
+
x.set(100) // animate to 100
|
|
841
|
+
x() // read current value (tracks as signal)
|
|
842
|
+
x.snap(50) // jump instantly, no animation
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
`useAnimated` is the hook version (auto-cleanup on unmount). `animated` is the standalone version for use outside components.
|
|
846
|
+
|
|
847
|
+
### Interpolators
|
|
848
|
+
|
|
849
|
+
```js
|
|
850
|
+
spring({ frequency: 2, damping: 0.3 }) // underdamped spring (bouncy)
|
|
851
|
+
spring({ damping: 1 }) // critically damped (no bounce)
|
|
852
|
+
ease(300) // 300ms ease-out-cubic
|
|
853
|
+
ease(500, linear) // 500ms linear
|
|
854
|
+
decay({ deceleration: 0.998 }) // momentum-based decay
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
Switch interpolator mid-animation:
|
|
858
|
+
|
|
859
|
+
```js
|
|
860
|
+
x.setInterpolator(ease(200))
|
|
861
|
+
x.set(newTarget)
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
### Easing functions
|
|
865
|
+
|
|
866
|
+
`linear`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeOutElastic()`, `easeOutBounce()`
|
|
867
|
+
|
|
868
|
+
### Tick callback
|
|
869
|
+
|
|
870
|
+
```js
|
|
871
|
+
x.onTick((value) => { /* called each frame while animating */ })
|
|
872
|
+
```
|
|
873
|
+
|
|
510
874
|
## Build
|
|
511
875
|
|
|
512
876
|
Uses esbuild. JSX configured with `jsxImportSource: 'trend'`.
|