@trendr/core 0.1.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/LICENSE +21 -0
- package/README.md +526 -0
- package/index.js +22 -0
- package/jsx-runtime.js +9 -0
- package/package.json +35 -0
- package/src/ansi.js +149 -0
- package/src/buffer.js +101 -0
- package/src/button.js +26 -0
- package/src/checkbox.js +28 -0
- package/src/components.js +13 -0
- package/src/diff.js +51 -0
- package/src/element.js +81 -0
- package/src/focus.js +135 -0
- package/src/hooks.js +63 -0
- package/src/hotkey.js +35 -0
- package/src/input.js +143 -0
- package/src/layout.js +312 -0
- package/src/list.js +46 -0
- package/src/modal.js +44 -0
- package/src/progress.js +21 -0
- package/src/radio.js +52 -0
- package/src/renderer.js +478 -0
- package/src/scheduler.js +55 -0
- package/src/scrollable-text.js +75 -0
- package/src/select.js +151 -0
- package/src/signal.js +159 -0
- package/src/spinner.js +21 -0
- package/src/table.js +51 -0
- package/src/tabs.js +30 -0
- package/src/text-area.js +286 -0
- package/src/text-input.js +131 -0
- package/src/toast.js +61 -0
- package/src/wrap.js +133 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jonathan Pyers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src=".github/trend.png" alt="trend" width="400" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">4-16x faster than ink and neo-blessed - <a href="bench/README.md">benchmarks</a></p>
|
|
6
|
+
|
|
7
|
+
Direct-mode TUI renderer with JSX, signals, and per-cell diffing.
|
|
8
|
+
|
|
9
|
+
ESM only. Requires esbuild (or similar) for JSX transformation.
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{ "jsx": "automatic", "jsxImportSource": "trend" }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
https://github.com/user-attachments/assets/d307ba1e-2b21-4f7d-8b1b-56252820db6c
|
|
16
|
+
|
|
17
|
+
https://github.com/user-attachments/assets/a5984747-f365-4fe3-a161-93101682ca42
|
|
18
|
+
|
|
19
|
+
https://github.com/user-attachments/assets/9c357c93-0299-4480-a969-61a54b49ec33
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
import { mount, createSignal, useInput } from 'trend'
|
|
25
|
+
|
|
26
|
+
function App() {
|
|
27
|
+
const [count, setCount] = createSignal(0)
|
|
28
|
+
|
|
29
|
+
useInput(({ key }) => {
|
|
30
|
+
if (key === 'up') setCount(c => c + 1)
|
|
31
|
+
if (key === 'down') setCount(c => c - 1)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<box style={{ flexDirection: 'column', padding: 1 }}>
|
|
36
|
+
<text style={{ color: 'cyan', bold: true }}>Count: {count()}</text>
|
|
37
|
+
<text style={{ color: 'gray' }}>up/down to change</text>
|
|
38
|
+
</box>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
mount(App)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`mount(Component, { stream?, stdin?, theme? })` enters alt screen, starts a 60fps render loop, and returns `{ unmount }`. Components are plain functions that return JSX. State is managed with signals - call the getter to read, the setter to write. The framework re-renders automatically when signals change.
|
|
46
|
+
|
|
47
|
+
### Theming
|
|
48
|
+
|
|
49
|
+
All built-in components use `accent` as the focus/highlight color (default: `'cyan'`). Override it globally via mount:
|
|
50
|
+
|
|
51
|
+
```jsx
|
|
52
|
+
mount(App, { theme: { accent: 'green' } })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Components read the accent with `useTheme()`. Use this in your own components to stay consistent:
|
|
56
|
+
|
|
57
|
+
```jsx
|
|
58
|
+
import { useTheme } from 'trend'
|
|
59
|
+
|
|
60
|
+
const { accent } = useTheme()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Individual components still accept explicit color props (e.g. `<Spinner color="magenta" />`) which override the theme.
|
|
64
|
+
|
|
65
|
+
## Signals
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { createSignal, createEffect, createMemo, batch, untrack, onCleanup } from 'trend'
|
|
69
|
+
|
|
70
|
+
const [value, setValue] = createSignal(0)
|
|
71
|
+
value() // read (tracks dependency)
|
|
72
|
+
setValue(1) // write
|
|
73
|
+
setValue(v => v + 1) // updater
|
|
74
|
+
|
|
75
|
+
createEffect(() => {
|
|
76
|
+
console.log(value()) // re-runs when value changes
|
|
77
|
+
return () => {} // optional cleanup
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const doubled = createMemo(() => value() * 2) // cached derived value
|
|
81
|
+
|
|
82
|
+
batch(() => { // coalesce multiple updates into one render
|
|
83
|
+
setValue(1)
|
|
84
|
+
setValue(2)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
untrack(() => value()) // read without tracking
|
|
88
|
+
onCleanup(() => {}) // runs when component unmounts or effect re-runs
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Layout
|
|
92
|
+
|
|
93
|
+
Two element types: `box` (container) and `text` (leaf).
|
|
94
|
+
|
|
95
|
+
```jsx
|
|
96
|
+
<box style={{
|
|
97
|
+
flexDirection: 'column', // 'column' (default) | 'row'
|
|
98
|
+
flexGrow: 1, // fill remaining space
|
|
99
|
+
gap: 1, // space between children
|
|
100
|
+
justifyContent: 'flex-start', // 'flex-start' | 'center' | 'flex-end'
|
|
101
|
+
alignItems: 'stretch', // 'stretch' | 'flex-start' | 'center' | 'flex-end'
|
|
102
|
+
width: 20, // fixed or '50%'
|
|
103
|
+
height: 10, // fixed or '25%'
|
|
104
|
+
minWidth: 5, maxWidth: 30,
|
|
105
|
+
minHeight: 2, maxHeight: 15,
|
|
106
|
+
padding: 1, // all sides
|
|
107
|
+
paddingX: 1, paddingY: 1, // axis
|
|
108
|
+
paddingTop: 1, paddingBottom: 1, paddingLeft: 1, paddingRight: 1,
|
|
109
|
+
margin: 1, // same variants as padding
|
|
110
|
+
border: 'round', // 'single' | 'double' | 'round' | 'bold'
|
|
111
|
+
borderColor: 'cyan',
|
|
112
|
+
bg: 'blue', // background color
|
|
113
|
+
}}>
|
|
114
|
+
|
|
115
|
+
<text style={{
|
|
116
|
+
color: 'cyan', // named, hex (#ff0000), or 256-color index
|
|
117
|
+
bg: 'black',
|
|
118
|
+
bold: true,
|
|
119
|
+
dim: true,
|
|
120
|
+
italic: true,
|
|
121
|
+
underline: true,
|
|
122
|
+
inverse: true,
|
|
123
|
+
strikethrough: true,
|
|
124
|
+
overflow: 'wrap', // 'wrap' (default) | 'truncate' | 'nowrap'
|
|
125
|
+
}}>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`Box`, `Text`, and `Spacer` are convenience wrappers:
|
|
129
|
+
|
|
130
|
+
```jsx
|
|
131
|
+
import { Box, Text, Spacer } from 'trend'
|
|
132
|
+
<Box style={{ flexDirection: 'row' }}><Text>hello</Text><Spacer /><Text>right</Text></Box>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Hooks
|
|
136
|
+
|
|
137
|
+
### useInput
|
|
138
|
+
|
|
139
|
+
Used in [counter](examples/counter.jsx), [dashboard](examples/dashboard.jsx), [explorer](examples/explorer.jsx), [chat](examples/chat.jsx), [modal-form](examples/modal-form.jsx), [components](examples/components.jsx), [focus-demo](examples/focus-demo.jsx)
|
|
140
|
+
|
|
141
|
+
```jsx
|
|
142
|
+
useInput((event) => {
|
|
143
|
+
// event.key: 'a', 'return', 'escape', 'up', 'down', 'left', 'right',
|
|
144
|
+
// 'tab', 'shift-tab', 'space', 'backspace', 'delete',
|
|
145
|
+
// 'home', 'end', 'pageup', 'pagedown', 'f1'-'f12'
|
|
146
|
+
// event.ctrl: boolean
|
|
147
|
+
// event.meta: boolean (alt/option key)
|
|
148
|
+
// event.raw: raw character string
|
|
149
|
+
// event.stopPropagation(): prevent other handlers from receiving this event
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Handlers fire in reverse registration order (innermost component first). Call `stopPropagation()` to consume the event.
|
|
154
|
+
|
|
155
|
+
### useHotkey
|
|
156
|
+
|
|
157
|
+
Declarative key binding. Parses `'ctrl+s'`, `'alt+enter'`, etc.
|
|
158
|
+
|
|
159
|
+
```jsx
|
|
160
|
+
import { useHotkey } from 'trend'
|
|
161
|
+
|
|
162
|
+
useHotkey('ctrl+s', () => save())
|
|
163
|
+
useHotkey('alt+enter', () => submit(), { when: () => isFocused })
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### useLayout
|
|
167
|
+
|
|
168
|
+
Returns the component's computed layout rectangle.
|
|
169
|
+
|
|
170
|
+
```jsx
|
|
171
|
+
const { x, y, width, height } = useLayout()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### useResize
|
|
175
|
+
|
|
176
|
+
```jsx
|
|
177
|
+
useResize(({ width, height }) => { /* terminal resized */ })
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### useInterval
|
|
181
|
+
|
|
182
|
+
Used in [dashboard](examples/dashboard.jsx)
|
|
183
|
+
|
|
184
|
+
```jsx
|
|
185
|
+
useInterval(() => tick(), 1000) // auto-cleaned on unmount
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### useStdout
|
|
189
|
+
|
|
190
|
+
```jsx
|
|
191
|
+
const stream = useStdout() // the output stream (process.stdout or custom)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### useTheme
|
|
195
|
+
|
|
196
|
+
Returns the current theme object. See [Theming](#theming).
|
|
197
|
+
|
|
198
|
+
```jsx
|
|
199
|
+
const { accent } = useTheme()
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### useFocus
|
|
203
|
+
|
|
204
|
+
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
|
+
|
|
206
|
+
Manages focus across multiple interactive regions. You register named items in the order you want tab to cycle through them. The focus manager tracks which name is currently active - it doesn't know anything about your components or layout.
|
|
207
|
+
|
|
208
|
+
```jsx
|
|
209
|
+
import { useFocus } from 'trend'
|
|
210
|
+
|
|
211
|
+
const fm = useFocus({ initial: 'input' })
|
|
212
|
+
|
|
213
|
+
// declaration order = tab order
|
|
214
|
+
fm.item('input') // tab stop 0
|
|
215
|
+
fm.item('list') // tab stop 1
|
|
216
|
+
fm.item('sidebar') // tab stop 2
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Then wire each component's `focused` prop to `fm.is()`, which returns true when that name is the active one:
|
|
220
|
+
|
|
221
|
+
```jsx
|
|
222
|
+
<TextInput focused={fm.is('input')} />
|
|
223
|
+
<List focused={fm.is('list')} />
|
|
224
|
+
<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
|
+
|
|
229
|
+
```jsx
|
|
230
|
+
fm.is('input') // boolean - is 'input' the active name?
|
|
231
|
+
fm.focus('list') // jump to 'list' programmatically
|
|
232
|
+
fm.current() // the currently active name
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Groups let you nest multiple items under one tab stop, navigable with j/k or up/down within the group:
|
|
236
|
+
|
|
237
|
+
```jsx
|
|
238
|
+
fm.group('settings', { items: ['theme', 'autosave', 'format'], navigate: 'updown', wrap: true })
|
|
239
|
+
// fm.is('theme'), fm.is('autosave'), etc. work within the group
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Stack-based focus for modals and drills - push saves the current focus and switches, pop restores it:
|
|
243
|
+
|
|
244
|
+
```jsx
|
|
245
|
+
fm.push('modal') // save current focus, switch to 'modal'
|
|
246
|
+
fm.pop() // restore previous focus
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### useToast
|
|
250
|
+
|
|
251
|
+
Used in [chat](examples/chat.jsx), [modal-form](examples/modal-form.jsx), [components](examples/components.jsx)
|
|
252
|
+
|
|
253
|
+
```jsx
|
|
254
|
+
import { useToast } from 'trend'
|
|
255
|
+
|
|
256
|
+
const toast = useToast({
|
|
257
|
+
duration: 2000, // ms, default 2000
|
|
258
|
+
position: 'bottom-right', // see positions below
|
|
259
|
+
margin: 1, // padding from screen edge, default 1
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
toast('saved')
|
|
263
|
+
|
|
264
|
+
// positions: 'top-left', 'top-center', 'top-right',
|
|
265
|
+
// 'center-left', 'center', 'center-right',
|
|
266
|
+
// 'bottom-left', 'bottom-center', 'bottom-right'
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Components
|
|
270
|
+
|
|
271
|
+
All interactive components accept a `focused` prop that controls whether they respond to keyboard input. When multiple components are on screen, only the focused one should capture keys - otherwise a keypress meant for a text input would also scroll a list. In practice you wire this to a focus manager:
|
|
272
|
+
|
|
273
|
+
```jsx
|
|
274
|
+
const fm = useFocus({ initial: 'search' })
|
|
275
|
+
fm.item('search')
|
|
276
|
+
fm.item('results')
|
|
277
|
+
|
|
278
|
+
<TextInput focused={fm.is('search')} />
|
|
279
|
+
<List focused={fm.is('results')} />
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### TextInput
|
|
283
|
+
|
|
284
|
+
Used in [explorer](examples/explorer.jsx), [modal-form](examples/modal-form.jsx), [focus-demo](examples/focus-demo.jsx)
|
|
285
|
+
|
|
286
|
+
Single-line text input with horizontal scrolling.
|
|
287
|
+
|
|
288
|
+
```jsx
|
|
289
|
+
<TextInput
|
|
290
|
+
focused={fm.is('search')}
|
|
291
|
+
placeholder="search..."
|
|
292
|
+
onChange={v => {}} // every keystroke
|
|
293
|
+
onSubmit={v => {}} // Enter
|
|
294
|
+
onCancel={() => {}} // Escape (only stopPropagates if provided)
|
|
295
|
+
/>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Keys: left/right, home/end, ctrl-a/e, ctrl-u/k/w, backspace, delete.
|
|
299
|
+
|
|
300
|
+
### TextArea
|
|
301
|
+
|
|
302
|
+
Used in [chat](examples/chat.jsx)
|
|
303
|
+
|
|
304
|
+
Multi-line text input. Auto-grows up to `maxHeight`, then scrolls.
|
|
305
|
+
|
|
306
|
+
```jsx
|
|
307
|
+
<TextArea
|
|
308
|
+
focused={fm.is('input')}
|
|
309
|
+
placeholder="write something..."
|
|
310
|
+
maxHeight={10} // default 10
|
|
311
|
+
onChange={v => {}} // every edit
|
|
312
|
+
onSubmit={v => {}} // Alt+Enter
|
|
313
|
+
onCancel={() => {}} // Escape
|
|
314
|
+
/>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Keys: Enter inserts newline. Up/down with sticky goal column. Home/end operate on display rows. Ctrl-u/k/w operate on logical lines.
|
|
318
|
+
|
|
319
|
+
### List
|
|
320
|
+
|
|
321
|
+
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)
|
|
322
|
+
|
|
323
|
+
Scrollable list with keyboard navigation.
|
|
324
|
+
|
|
325
|
+
```jsx
|
|
326
|
+
<List
|
|
327
|
+
items={data}
|
|
328
|
+
selected={selectedIndex} // controlled, or omit for internal state
|
|
329
|
+
onSelect={setIndex}
|
|
330
|
+
focused={fm.is('list')}
|
|
331
|
+
height={10} // defaults to layout height
|
|
332
|
+
header={<text>title</text>}
|
|
333
|
+
renderItem={(item, { selected, index, focused }) => (
|
|
334
|
+
<text style={{ bg: selected ? (focused ? accent : 'gray') : null }}>{item.name}</text>
|
|
335
|
+
)}
|
|
336
|
+
/>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Keys: j/k or up/down, g/G for top/bottom, ctrl-d/u half page, ctrl-f/b full page, pageup/pagedown.
|
|
340
|
+
|
|
341
|
+
### Table
|
|
342
|
+
|
|
343
|
+
Used in [components](examples/components.jsx)
|
|
344
|
+
|
|
345
|
+
Column-based data table. Uses List internally.
|
|
346
|
+
|
|
347
|
+
```jsx
|
|
348
|
+
<Table
|
|
349
|
+
columns={[
|
|
350
|
+
{ header: 'Name', key: 'name', flexGrow: 1 },
|
|
351
|
+
{ header: 'Size', key: 'size', width: 10, color: 'gray', paddingX: 1 },
|
|
352
|
+
{ header: 'Type', render: (row, sel) => row.type.toUpperCase(), width: 8 },
|
|
353
|
+
]}
|
|
354
|
+
data={rows}
|
|
355
|
+
selected={selectedRow}
|
|
356
|
+
onSelect={setRow}
|
|
357
|
+
focused={fm.is('table')}
|
|
358
|
+
/>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Tabs
|
|
362
|
+
|
|
363
|
+
Used in [chat](examples/chat.jsx)
|
|
364
|
+
|
|
365
|
+
```jsx
|
|
366
|
+
<Tabs
|
|
367
|
+
items={['general', 'settings', 'logs']}
|
|
368
|
+
selected={activeTab}
|
|
369
|
+
onSelect={setTab}
|
|
370
|
+
focused={fm.is('tabs')}
|
|
371
|
+
/>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Keys: left/right, tab/shift-tab. Wraps around.
|
|
375
|
+
|
|
376
|
+
### Select
|
|
377
|
+
|
|
378
|
+
Used in [modal-form](examples/modal-form.jsx), [components](examples/components.jsx), [focus-demo](examples/focus-demo.jsx)
|
|
379
|
+
|
|
380
|
+
Dropdown selector. Can render inline or as overlay.
|
|
381
|
+
|
|
382
|
+
```jsx
|
|
383
|
+
<Select
|
|
384
|
+
items={['red', 'green', 'blue']}
|
|
385
|
+
selected={color}
|
|
386
|
+
onSelect={setColor}
|
|
387
|
+
focused={fm.is('color')}
|
|
388
|
+
overlay={false} // true renders as floating overlay
|
|
389
|
+
placeholder="pick one..."
|
|
390
|
+
renderItem={(item, { selected, index }) => <text>{item}</text>}
|
|
391
|
+
style={{
|
|
392
|
+
border: 'single', borderColor: 'green', bg: 'black',
|
|
393
|
+
cursorBg: 'green', cursorTextColor: 'black',
|
|
394
|
+
color: null, focusedColor: 'green',
|
|
395
|
+
}}
|
|
396
|
+
/>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Keys: j/k or up/down to navigate, enter/space to select, escape to close.
|
|
400
|
+
|
|
401
|
+
### Checkbox
|
|
402
|
+
|
|
403
|
+
Used in [modal-form](examples/modal-form.jsx), [components](examples/components.jsx), [focus-demo](examples/focus-demo.jsx)
|
|
404
|
+
|
|
405
|
+
```jsx
|
|
406
|
+
<Checkbox
|
|
407
|
+
checked={isChecked}
|
|
408
|
+
label="Enable feature"
|
|
409
|
+
onChange={setChecked} // (newState: boolean) => void
|
|
410
|
+
focused={fm.is('feature')}
|
|
411
|
+
/>
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Keys: space or enter to toggle. Renders `[x]` / `[ ]`.
|
|
415
|
+
|
|
416
|
+
### Radio
|
|
417
|
+
|
|
418
|
+
Used in [modal-form](examples/modal-form.jsx), [components](examples/components.jsx), [focus-demo](examples/focus-demo.jsx)
|
|
419
|
+
|
|
420
|
+
```jsx
|
|
421
|
+
<Radio
|
|
422
|
+
options={['small', 'medium', 'large']}
|
|
423
|
+
selected={size}
|
|
424
|
+
onSelect={setSize}
|
|
425
|
+
focused={fm.is('size')}
|
|
426
|
+
/>
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Keys: j/k or up/down, enter/space to select. Renders circle symbols.
|
|
430
|
+
|
|
431
|
+
### ProgressBar
|
|
432
|
+
|
|
433
|
+
Used in [dashboard](examples/dashboard.jsx), [components](examples/components.jsx)
|
|
434
|
+
|
|
435
|
+
```jsx
|
|
436
|
+
<ProgressBar
|
|
437
|
+
value={0.65} // 0 to 1
|
|
438
|
+
width={20} // characters, default 20
|
|
439
|
+
color="red" // overrides theme accent
|
|
440
|
+
label="65%" // optional text after bar
|
|
441
|
+
/>
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Spinner
|
|
445
|
+
|
|
446
|
+
Used in [components](examples/components.jsx)
|
|
447
|
+
|
|
448
|
+
```jsx
|
|
449
|
+
<Spinner
|
|
450
|
+
label="loading..."
|
|
451
|
+
color="magenta" // overrides theme accent
|
|
452
|
+
interval={80} // ms, default 80
|
|
453
|
+
/>
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Button
|
|
457
|
+
|
|
458
|
+
Used in [modal-form](examples/modal-form.jsx)
|
|
459
|
+
|
|
460
|
+
Focusable button. Enter or space to activate.
|
|
461
|
+
|
|
462
|
+
```jsx
|
|
463
|
+
<Button
|
|
464
|
+
label="save"
|
|
465
|
+
onPress={() => save()}
|
|
466
|
+
focused={fm.is('save')}
|
|
467
|
+
variant="dim" // optional, grays out when unfocused
|
|
468
|
+
/>
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Modal
|
|
472
|
+
|
|
473
|
+
Used in [modal-form](examples/modal-form.jsx), [components](examples/components.jsx), [focus-demo](examples/focus-demo.jsx)
|
|
474
|
+
|
|
475
|
+
Centered overlay with dimmed backdrop. Height is driven by content.
|
|
476
|
+
|
|
477
|
+
```jsx
|
|
478
|
+
<Modal
|
|
479
|
+
open={isOpen}
|
|
480
|
+
onClose={() => setOpen(false)}
|
|
481
|
+
title="Confirm"
|
|
482
|
+
width={40} // default 40
|
|
483
|
+
>
|
|
484
|
+
<text>Are you sure?</text>
|
|
485
|
+
<Button label="ok" onPress={() => setOpen(false)} focused={fm.is('ok')} />
|
|
486
|
+
</Modal>
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Keys: escape to close.
|
|
490
|
+
|
|
491
|
+
### ScrollableText
|
|
492
|
+
|
|
493
|
+
Used in [explorer](examples/explorer.jsx), [reader](examples/reader.jsx), [highlight](examples/highlight.jsx)
|
|
494
|
+
|
|
495
|
+
Scrollable text viewer with optional scrollbar. Content can include ANSI escape sequences (SGR) - colors, bold, dim, etc. are parsed and rendered correctly. This means you can pipe output from any syntax highlighter (shiki, cli-highlight, etc.) directly into `content`.
|
|
496
|
+
|
|
497
|
+
```jsx
|
|
498
|
+
<ScrollableText
|
|
499
|
+
content={longText}
|
|
500
|
+
focused={fm.is('preview')}
|
|
501
|
+
scrollOffset={offset} // controlled, or omit for internal state
|
|
502
|
+
onScroll={setOffset}
|
|
503
|
+
scrollbar={true} // default false
|
|
504
|
+
wrap={false} // default true, set false for horizontal scroll
|
|
505
|
+
/>
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Keys: same as List (j/k, g/G, ctrl-d/u, ctrl-f/b, pageup/pagedown).
|
|
509
|
+
|
|
510
|
+
## Build
|
|
511
|
+
|
|
512
|
+
Uses esbuild. JSX configured with `jsxImportSource: 'trend'`.
|
|
513
|
+
|
|
514
|
+
```
|
|
515
|
+
node esbuild.config.js
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Examples run via npm scripts:
|
|
519
|
+
|
|
520
|
+
```
|
|
521
|
+
npm run counter
|
|
522
|
+
npm run chat
|
|
523
|
+
npm run dashboard
|
|
524
|
+
npm run explorer
|
|
525
|
+
npm run highlight
|
|
526
|
+
```
|
package/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { mount } from './src/renderer.js'
|
|
2
|
+
export { createSignal } from './src/signal.js'
|
|
3
|
+
export { createEffect, createMemo, batch, untrack, onCleanup } from './src/signal.js'
|
|
4
|
+
export { useInput, useResize, useInterval, useLayout, useStdout, useTitle, useTheme } from './src/hooks.js'
|
|
5
|
+
export { Box, Text, Spacer } from './src/components.js'
|
|
6
|
+
export { TextInput } from './src/text-input.js'
|
|
7
|
+
export { TextArea } from './src/text-area.js'
|
|
8
|
+
export { List } from './src/list.js'
|
|
9
|
+
export { Table } from './src/table.js'
|
|
10
|
+
export { Tabs } from './src/tabs.js'
|
|
11
|
+
export { Select } from './src/select.js'
|
|
12
|
+
export { Checkbox } from './src/checkbox.js'
|
|
13
|
+
export { ProgressBar } from './src/progress.js'
|
|
14
|
+
export { Spinner } from './src/spinner.js'
|
|
15
|
+
export { Modal } from './src/modal.js'
|
|
16
|
+
export { Radio } from './src/radio.js'
|
|
17
|
+
export { Button } from './src/button.js'
|
|
18
|
+
export { ScrollableText } from './src/scrollable-text.js'
|
|
19
|
+
export { useFocus } from './src/focus.js'
|
|
20
|
+
export { useHotkey } from './src/hotkey.js'
|
|
21
|
+
export { useToast } from './src/toast.js'
|
|
22
|
+
export { Fragment } from './src/element.js'
|
package/jsx-runtime.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@trendr/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "direct-mode TUI renderer with JSX, signals, and per-cell diffing",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./index.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"index.js",
|
|
11
|
+
"jsx-runtime.js"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./index.js",
|
|
15
|
+
"./jsx-runtime": "./jsx-runtime.js",
|
|
16
|
+
"./jsx-dev-runtime": "./jsx-runtime.js"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node test/test.js && node test/test-e2e.js && node test/test-mount.js && node test/test-render.js",
|
|
20
|
+
"build": "node esbuild.config.js",
|
|
21
|
+
"counter": "node esbuild.config.js && node dist/counter.js",
|
|
22
|
+
"dashboard": "node esbuild.config.js && node dist/dashboard.js",
|
|
23
|
+
"chat": "node esbuild.config.js && node dist/chat.js",
|
|
24
|
+
"explorer": "node esbuild.config.js && node dist/explorer.js",
|
|
25
|
+
"layout": "node esbuild.config.js && node dist/layout.js",
|
|
26
|
+
"components": "node esbuild.config.js && node dist/components.js",
|
|
27
|
+
"reader": "node esbuild.config.js && node dist/reader.js",
|
|
28
|
+
"focus-demo": "node esbuild.config.js && node dist/focus-demo.js",
|
|
29
|
+
"modal-form": "node esbuild.config.js && node dist/modal-form.js",
|
|
30
|
+
"highlight": "node esbuild.config.js && node dist/highlight.js src"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"esbuild": "^0.25.0"
|
|
34
|
+
}
|
|
35
|
+
}
|