@trendr/core 0.2.10 → 0.4.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 +284 -24
- package/index.js +6 -2
- package/package.json +29 -34
- package/src/animation.js +25 -12
- package/src/ansi.js +7 -0
- package/src/buffer.js +63 -50
- package/src/button.js +2 -2
- package/src/checkbox.js +2 -2
- package/src/diff-engine.js +313 -0
- package/src/diff-view.js +238 -0
- package/src/diff.js +2 -2
- package/src/dropdown.js +147 -0
- package/src/focus.js +89 -27
- package/src/hooks.js +43 -25
- package/src/hotkey.js +51 -16
- package/src/input.js +211 -72
- package/src/layout.js +34 -13
- package/src/list.js +29 -16
- package/src/markdown.js +177 -0
- package/src/menu.js +13 -6
- package/src/menubar.js +46 -115
- package/src/miller-nav.js +36 -11
- package/src/modal.js +3 -1
- package/src/pick-list.js +16 -17
- package/src/progress.js +3 -3
- package/src/radio.js +16 -5
- package/src/renderer.js +315 -58
- package/src/scheduler.js +28 -10
- package/src/scroll-box.js +12 -11
- package/src/scrollable-text.js +3 -2
- package/src/select.js +65 -173
- package/src/selection.js +92 -0
- package/src/shimmer.js +2 -2
- package/src/signal.js +20 -3
- package/src/table.js +8 -27
- package/src/tabs.js +10 -7
- package/src/task.js +3 -3
- package/src/text-area.js +96 -41
- package/src/text-input.js +70 -28
- package/src/wrap.js +205 -84
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
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
|
-
|
|
11
|
+
No dependencies. In the included [benchmarks](bench/README.md), median frame times are 4.5x faster than ink on the dashboard and list-scrolling scenarios, 10.5x on ink's own rerender benchmark, and 24.8x on the resize-storm worst case; against neo-blessed the range is 1.9x to 8.6x. In the single-cell scenario (a 10,000-cell screen where only a corner counter changes), per-cell diffing writes 17 bytes per frame where ink writes about 9.9KB - though ink defers its terminal writes, so bytes written is not directly comparable between the two (see the notes in bench/README.md).
|
|
12
12
|
|
|
13
13
|
https://github.com/user-attachments/assets/6e84bb4b-a99e-46f2-a235-1ee4be62c0ae
|
|
14
14
|
|
|
@@ -21,7 +21,7 @@ npm i @trendr/core
|
|
|
21
21
|
Requires esbuild (or similar) for JSX transformation.
|
|
22
22
|
|
|
23
23
|
```json
|
|
24
|
-
{ "jsx": "automatic", "jsxImportSource": "
|
|
24
|
+
{ "jsx": "automatic", "jsxImportSource": "@trendr/core" }
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
```jsx
|
|
@@ -46,7 +46,40 @@ function App() {
|
|
|
46
46
|
mount(App)
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
`mount(Component,
|
|
49
|
+
`mount(Component, options)` starts the render loop. Frames render on demand when signals change, capped at 60fps.
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
|
|
53
|
+
- `stream` - output stream, default `process.stdout`
|
|
54
|
+
- `stdin` - input stream, default `process.stdin`
|
|
55
|
+
- `title` - terminal window title
|
|
56
|
+
- `theme` - theme object, see [Theming](#theming)
|
|
57
|
+
- `onExit` - called after ctrl-c unmounts the app; when provided it replaces the default `process.exit(0)`
|
|
58
|
+
- `altScreen` - enter the alternate screen buffer, default `true`
|
|
59
|
+
- `inline` - inline mode, default `false` (see [Inline mode](#inline-mode))
|
|
60
|
+
|
|
61
|
+
Returns `{ unmount, repaint, getBuffer }`. `unmount` tears down input handling and restores the terminal, `repaint` forces a full repaint, `getBuffer` returns the last rendered cell buffer.
|
|
62
|
+
|
|
63
|
+
### Inline mode
|
|
64
|
+
|
|
65
|
+
`mount(App, { inline: true })` renders below the shell prompt instead of taking over the screen. The UI is split into a committed transcript and a live region. Committed content goes inside `<Scrollback items={...} render={...} />`: items are append-only, each new item is rendered once, printed into native terminal scrollback, and never touched again, so it scrolls and copies like normal terminal output. Everything outside `Scrollback` is the live region, which re-renders in place below the transcript.
|
|
66
|
+
|
|
67
|
+
```jsx
|
|
68
|
+
import { mount, Scrollback } from '@trendr/core'
|
|
69
|
+
|
|
70
|
+
function Chat() {
|
|
71
|
+
return (
|
|
72
|
+
<box style={{ flexDirection: 'column' }}>
|
|
73
|
+
<Scrollback items={history()} render={(msg) => <Message {...msg} />} />
|
|
74
|
+
<TextArea onSubmit={send} />
|
|
75
|
+
</box>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
mount(Chat, { inline: true })
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
While an overlay (such as a Modal) is open, inline mode temporarily switches to the alternate screen and restores the transcript when it closes, so native scrollback is preserved. See [inline-chat](examples/inline-chat.jsx) for a full example.
|
|
50
83
|
|
|
51
84
|
### Theming
|
|
52
85
|
|
|
@@ -56,6 +89,8 @@ Pass a theme object to `mount` to configure global defaults:
|
|
|
56
89
|
mount(App, {
|
|
57
90
|
theme: {
|
|
58
91
|
accent: 'green', // focus/highlight color, default 'cyan'
|
|
92
|
+
accentText: 'black', // text drawn on accent backgrounds (cursor rows, focused buttons, active tabs), default 'black'
|
|
93
|
+
muted: 'gray', // de-emphasized text (placeholders, hints, inactive items, scrollbar rails), default 'gray'
|
|
59
94
|
cursor: {
|
|
60
95
|
blink: true, // default false
|
|
61
96
|
rate: 530, // blink interval ms, default 530
|
|
@@ -72,7 +107,7 @@ Components read the theme with `useTheme()`:
|
|
|
72
107
|
```jsx
|
|
73
108
|
import { useTheme } from '@trendr/core'
|
|
74
109
|
|
|
75
|
-
const { accent } = useTheme()
|
|
110
|
+
const { accent, accentText, muted } = useTheme()
|
|
76
111
|
```
|
|
77
112
|
|
|
78
113
|
Individual components still accept explicit color props (e.g. `<Spinner color="magenta" />`) which override the theme.
|
|
@@ -302,6 +337,26 @@ stdout.write(altScreen + hideCursor)
|
|
|
302
337
|
repaint()
|
|
303
338
|
```
|
|
304
339
|
|
|
340
|
+
### useTitle
|
|
341
|
+
|
|
342
|
+
Sets the terminal window title.
|
|
343
|
+
|
|
344
|
+
```jsx
|
|
345
|
+
useTitle('my app')
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### useFrameStats
|
|
349
|
+
|
|
350
|
+
Returns stats for the last rendered frame.
|
|
351
|
+
|
|
352
|
+
```jsx
|
|
353
|
+
const { changed, total, bytes, fps } = useFrameStats()
|
|
354
|
+
// changed: cells that differed from the previous frame
|
|
355
|
+
// total: cells in the buffer (width * height)
|
|
356
|
+
// bytes: bytes written to the stream for the frame
|
|
357
|
+
// fps: rolling frames-per-second estimate
|
|
358
|
+
```
|
|
359
|
+
|
|
305
360
|
### useTheme
|
|
306
361
|
|
|
307
362
|
Returns the current theme object. See [Theming](#theming).
|
|
@@ -310,6 +365,20 @@ Returns the current theme object. See [Theming](#theming).
|
|
|
310
365
|
const { accent } = useTheme()
|
|
311
366
|
```
|
|
312
367
|
|
|
368
|
+
### useCursor
|
|
369
|
+
|
|
370
|
+
Drives the text cursor for input-style components using the theme's cursor config (blink, style, colors). Pass an optional per-component cursor config and whether the component is focused.
|
|
371
|
+
|
|
372
|
+
```jsx
|
|
373
|
+
const { config, visible, cursorStyle, reset } = useCursor(cursorProp, focused)
|
|
374
|
+
|
|
375
|
+
// cursorStyle(): style object for the cursor cell (null when hidden or unfocused)
|
|
376
|
+
// visible(): blink state signal
|
|
377
|
+
// reset(): restart the blink cycle (call on keystrokes)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The built-in inputs (TextInput, TextArea, PickList) use this internally and accept a `cursor` prop that is forwarded here.
|
|
381
|
+
|
|
313
382
|
### useFocus
|
|
314
383
|
|
|
315
384
|
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)
|
|
@@ -320,6 +389,9 @@ Register named items in tab order. The focus manager tracks which is active.
|
|
|
320
389
|
import { useFocus } from '@trendr/core'
|
|
321
390
|
|
|
322
391
|
const fm = useFocus({ initial: 'input' })
|
|
392
|
+
// options: initial (name focused at mount),
|
|
393
|
+
// cycle: 'tab' (default) handles tab/shift-tab cycling;
|
|
394
|
+
// any other value disables it so you can drive focus yourself
|
|
323
395
|
|
|
324
396
|
// declaration order = tab order
|
|
325
397
|
fm.item('input') // tab stop 0
|
|
@@ -356,6 +428,16 @@ fm.push('modal') // save current focus, switch to 'modal'
|
|
|
356
428
|
fm.pop() // restore previous focus
|
|
357
429
|
```
|
|
358
430
|
|
|
431
|
+
### useFocusTrap
|
|
432
|
+
|
|
433
|
+
While active, stops tab/shift-tab from propagating past the calling component, so focus managers registered earlier in the tree do not also cycle. Handlers fire innermost-first: a `useFocus` inside the trapped subtree still receives the tab first, then the trap halts it. Useful for modals with their own focus manager.
|
|
434
|
+
|
|
435
|
+
```jsx
|
|
436
|
+
import { useFocusTrap } from '@trendr/core'
|
|
437
|
+
|
|
438
|
+
useFocusTrap(modalOpen)
|
|
439
|
+
```
|
|
440
|
+
|
|
359
441
|
### useToast
|
|
360
442
|
|
|
361
443
|
Used in [chat](examples/chat.jsx), [modal-form](examples/modal-form.jsx), [components](examples/components.jsx)
|
|
@@ -394,6 +476,10 @@ fm.item('results')
|
|
|
394
476
|
<List focused={fm.is('results')} />
|
|
395
477
|
```
|
|
396
478
|
|
|
479
|
+
**Callback conventions.** `onChange(value)` fires when a committed value changes (Select, Radio, Tabs, Checkbox, TextInput). `onSubmit(item)` or `onSubmit(payload)` fires when an item or action is committed (Menu, PickList, MenuBar, TextArea, Button). `onSelect(index)` fires when the cursor moves in list-navigation components (List, Table, Menu) and pairs with `selected` for controlled use; `onCursorChange(item, index)` is the same notification with the item included.
|
|
480
|
+
|
|
481
|
+
**Focused defaults.** Navigation components (List, Table, Menu, PickList, MillerNav) default to `focused: true` so a lone component responds immediately. Activation components (Select, Checkbox, Radio, Button, MenuBar) default to `focused: false` so space/enter can't trigger unfocused controls. Pass `focused` explicitly in multi-widget apps. List gates mouse handling on `focused` and keyboard handling on `interactive` - parents like PickList set `interactive: false` to own the keyboard while leaving mouse handling local.
|
|
482
|
+
|
|
397
483
|
### TextInput
|
|
398
484
|
|
|
399
485
|
Used in [explorer](examples/explorer.jsx), [modal-form](examples/modal-form.jsx), [focus-demo](examples/focus-demo.jsx)
|
|
@@ -404,6 +490,9 @@ Single-line text input with horizontal scrolling.
|
|
|
404
490
|
<TextInput
|
|
405
491
|
focused={fm.is('search')}
|
|
406
492
|
placeholder="search..."
|
|
493
|
+
initialValue="prefill" // starting value
|
|
494
|
+
clearOnSubmit={false} // reset to empty on Enter (default false)
|
|
495
|
+
cursor={{ blink: true }} // per-component cursor config (overrides theme)
|
|
407
496
|
onChange={v => {}} // every keystroke
|
|
408
497
|
onSubmit={v => {}} // Enter
|
|
409
498
|
onCancel={() => {}} // Escape (only stopPropagates if provided)
|
|
@@ -422,10 +511,17 @@ Multi-line text input. Auto-grows up to `maxHeight`, then scrolls.
|
|
|
422
511
|
<TextArea
|
|
423
512
|
focused={fm.is('input')}
|
|
424
513
|
placeholder="write something..."
|
|
425
|
-
maxHeight={10}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
514
|
+
maxHeight={10} // default 10
|
|
515
|
+
value={draft()} // controlled value (optional)
|
|
516
|
+
submitOnEnter={false} // default false: Alt+Enter submits, Enter inserts newline.
|
|
517
|
+
// true flips it: Enter submits, Shift/Alt+Enter inserts newline
|
|
518
|
+
clearOnSubmit={true} // reset to empty on submit (default true)
|
|
519
|
+
cursor={{ blink: true }} // per-component cursor config (overrides theme)
|
|
520
|
+
onChange={(v, prev) => {}} // every edit, receives new and previous value
|
|
521
|
+
onSubmit={v => {}} // submit key (see submitOnEnter)
|
|
522
|
+
onCancel={() => {}} // Escape
|
|
523
|
+
onKeyDown={event => {}} // raw key hook before internal handling;
|
|
524
|
+
// return true to consume the key (event.value is the current text)
|
|
429
525
|
/>
|
|
430
526
|
```
|
|
431
527
|
|
|
@@ -442,12 +538,15 @@ Scrollable list with keyboard navigation.
|
|
|
442
538
|
items={data}
|
|
443
539
|
selected={selectedIndex} // controlled, or omit for internal state
|
|
444
540
|
onSelect={setIndex}
|
|
541
|
+
onCursorChange={(item, index) => {}} // fires when the highlighted item changes
|
|
445
542
|
focused={fm.is('list')}
|
|
446
543
|
scrollbar={true} // default false
|
|
447
544
|
scrolloff={2} // items of margin from edges when scrolling (default 2)
|
|
448
545
|
interactive={true} // handle keyboard input (default: same as focused)
|
|
449
546
|
header={<text>title</text>}
|
|
450
547
|
headerHeight={1} // default 1, rows the header occupies
|
|
548
|
+
stickyHeader={false} // keep the header pinned while the list scrolls (default false)
|
|
549
|
+
gap={0} // blank rows between items (default 0)
|
|
451
550
|
renderItem={(item, { selected, index, focused }) => (
|
|
452
551
|
<text style={{ bg: selected ? (focused ? accent : 'gray') : null }}>{item.name}</text>
|
|
453
552
|
)}
|
|
@@ -483,12 +582,12 @@ Filterable list with live search. Text input at the top filters a scrollable lis
|
|
|
483
582
|
items={data}
|
|
484
583
|
focused={fm.is('search')}
|
|
485
584
|
placeholder="search..."
|
|
486
|
-
|
|
585
|
+
onSubmit={item => {}} // Enter on highlighted item
|
|
487
586
|
onCancel={() => {}} // Escape
|
|
488
587
|
onChange={query => {}} // every keystroke in the filter
|
|
489
|
-
|
|
588
|
+
clearOnSubmit={false} // reset filter on submit (default false)
|
|
490
589
|
scrollbar={true} // default false
|
|
491
|
-
scrolloff={
|
|
590
|
+
scrolloff={0} // items of margin from edges (default 0)
|
|
492
591
|
gap={1} // space between input and list (default 0)
|
|
493
592
|
filter={(query, item) => {}} // custom filter (default: case-insensitive includes)
|
|
494
593
|
/>
|
|
@@ -513,6 +612,31 @@ Multi-row items with `renderItem`, `itemHeight`, and `itemGap`:
|
|
|
513
612
|
|
|
514
613
|
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
614
|
|
|
615
|
+
### Menu
|
|
616
|
+
|
|
617
|
+
Used in [inline-chat](examples/inline-chat.jsx)
|
|
618
|
+
|
|
619
|
+
Windowed single-select list with no text input of its own. Shows at most `maxVisible` rows and scrolls as the cursor moves. It pairs with an external input, such as a slash-command palette above a TextArea: render it near the input and, while focused, it intercepts up/down/enter before the input sees them.
|
|
620
|
+
|
|
621
|
+
```jsx
|
|
622
|
+
<Menu
|
|
623
|
+
items={commands}
|
|
624
|
+
selected={index} // controlled, or omit for internal state
|
|
625
|
+
onSelect={setIndex} // cursor moved
|
|
626
|
+
onSubmit={(item, index) => {}} // Enter
|
|
627
|
+
onCancel={() => {}} // Escape (only stopPropagates if provided)
|
|
628
|
+
focused={showPalette}
|
|
629
|
+
maxVisible={5} // default 5
|
|
630
|
+
scrolloff={2} // default 2
|
|
631
|
+
itemHeight={1} // rows per item (default 1)
|
|
632
|
+
gap={0} // blank rows between items (default 0)
|
|
633
|
+
arrow="›" // marker before the active item (default '›')
|
|
634
|
+
renderItem={(item, { active }) => <text>{item.name}</text>}
|
|
635
|
+
/>
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
Keys: up/down or ctrl-p/ctrl-n, enter to submit, escape to cancel.
|
|
639
|
+
|
|
516
640
|
### Table
|
|
517
641
|
|
|
518
642
|
Used in [components](examples/components.jsx), [custom-table](examples/custom-table.jsx)
|
|
@@ -532,6 +656,12 @@ Column-based data table. Uses List internally.
|
|
|
532
656
|
focused={fm.is('table')}
|
|
533
657
|
separator={true} // horizontal rule below header
|
|
534
658
|
separatorChars={{ left: '', fill: '─', right: '' }} // customizable
|
|
659
|
+
columnGap={1} // spaces between columns (default 1)
|
|
660
|
+
stickyHeader={false} // pin header row while scrolling (default false)
|
|
661
|
+
gap={0} // blank rows between data rows (default 0)
|
|
662
|
+
itemHeight={1} // rows per data row, for multi-row rendering (default 1)
|
|
663
|
+
scrolloff={2} // rows of margin from edges when scrolling (default 2)
|
|
664
|
+
scrollbar={false} // default false
|
|
535
665
|
/>
|
|
536
666
|
```
|
|
537
667
|
|
|
@@ -560,13 +690,42 @@ Used in [chat](examples/chat.jsx)
|
|
|
560
690
|
<Tabs
|
|
561
691
|
items={['general', 'settings', 'logs']}
|
|
562
692
|
selected={activeTab}
|
|
563
|
-
|
|
693
|
+
onChange={setTab}
|
|
564
694
|
focused={fm.is('tabs')}
|
|
565
695
|
/>
|
|
566
696
|
```
|
|
567
697
|
|
|
568
698
|
Keys: left/right, tab/shift-tab. Wraps around.
|
|
569
699
|
|
|
700
|
+
### MenuBar
|
|
701
|
+
|
|
702
|
+
Used in [menubar](examples/menubar.jsx)
|
|
703
|
+
|
|
704
|
+
Horizontal menu bar with dropdown submenus rendered as overlays. Menu and item hotkeys are underlined in their labels and activate directly.
|
|
705
|
+
|
|
706
|
+
```jsx
|
|
707
|
+
<MenuBar
|
|
708
|
+
items={[
|
|
709
|
+
{
|
|
710
|
+
label: 'File',
|
|
711
|
+
hotkey: 'f',
|
|
712
|
+
children: [
|
|
713
|
+
{ label: 'New', hotkey: 'n' },
|
|
714
|
+
{ label: 'Open', hotkey: 'o', value: 'open-file' },
|
|
715
|
+
],
|
|
716
|
+
},
|
|
717
|
+
{ label: 'Edit', hotkey: 'e', children: [/* ... */] },
|
|
718
|
+
]}
|
|
719
|
+
focused={fm.is('menu')}
|
|
720
|
+
maxVisible={10} // dropdown rows before scrolling (default 10)
|
|
721
|
+
onSubmit={({ menu, item, value }) => {}}
|
|
722
|
+
hotkeyColor="cyan" // hotkey letter color (default: theme accent)
|
|
723
|
+
style={{}} // pass-through style for the bar row
|
|
724
|
+
/>
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
Keys: h/l or left/right to move between menus, enter/space to open, j/k or up/down inside a dropdown, enter/space to select, escape to close. Pressing a hotkey letter opens that menu or selects that item.
|
|
728
|
+
|
|
570
729
|
### Select
|
|
571
730
|
|
|
572
731
|
Used in [modal-form](examples/modal-form.jsx), [components](examples/components.jsx), [focus-demo](examples/focus-demo.jsx)
|
|
@@ -577,9 +736,10 @@ Dropdown selector. Can render inline or as overlay.
|
|
|
577
736
|
<Select
|
|
578
737
|
items={['red', 'green', 'blue']}
|
|
579
738
|
selected={color}
|
|
580
|
-
|
|
739
|
+
onChange={setColor}
|
|
581
740
|
focused={fm.is('color')}
|
|
582
741
|
overlay={false} // true renders as floating overlay
|
|
742
|
+
maxVisible={10} // rows shown before the dropdown scrolls (default 10)
|
|
583
743
|
placeholder="pick one..."
|
|
584
744
|
openIcon="▲" // default ▲
|
|
585
745
|
closedIcon="▼" // default ▼
|
|
@@ -619,7 +779,7 @@ Used in [modal-form](examples/modal-form.jsx), [components](examples/components.
|
|
|
619
779
|
<Radio
|
|
620
780
|
options={['small', 'medium', 'large']}
|
|
621
781
|
selected={size}
|
|
622
|
-
|
|
782
|
+
onChange={setSize}
|
|
623
783
|
focused={fm.is('size')}
|
|
624
784
|
/>
|
|
625
785
|
```
|
|
@@ -738,7 +898,8 @@ Centered overlay with dimmed backdrop. Height is driven by content.
|
|
|
738
898
|
open={isOpen}
|
|
739
899
|
onClose={() => setOpen(false)}
|
|
740
900
|
title="Confirm"
|
|
741
|
-
width={40}
|
|
901
|
+
width={40} // default 40
|
|
902
|
+
border="round" // 'single' | 'double' | 'round' | 'bold' (default 'round')
|
|
742
903
|
>
|
|
743
904
|
<text>Are you sure?</text>
|
|
744
905
|
<Button label="ok" onPress={() => setOpen(false)} focused={fm.is('ok')} />
|
|
@@ -747,6 +908,34 @@ Centered overlay with dimmed backdrop. Height is driven by content.
|
|
|
747
908
|
|
|
748
909
|
Keys: escape to close.
|
|
749
910
|
|
|
911
|
+
### registerOverlay
|
|
912
|
+
|
|
913
|
+
The mechanism behind Modal, Select's dropdown mode, and toasts. Registers an element to be laid out and painted above the main tree for the current frame. Overlays are collected fresh every frame, so call it during render and gate it on your own open state.
|
|
914
|
+
|
|
915
|
+
```jsx
|
|
916
|
+
import { registerOverlay } from '@trendr/core'
|
|
917
|
+
|
|
918
|
+
function Palette({ open }) {
|
|
919
|
+
if (open) {
|
|
920
|
+
registerOverlay(
|
|
921
|
+
<box style={{ position: 'absolute', top: 2, left: 4, width: 40, border: 'round' }}>
|
|
922
|
+
<text>palette</text>
|
|
923
|
+
</box>,
|
|
924
|
+
{ backdrop: true },
|
|
925
|
+
)
|
|
926
|
+
}
|
|
927
|
+
return null
|
|
928
|
+
}
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
Options:
|
|
932
|
+
|
|
933
|
+
- `backdrop` - lay the overlay out over the full screen and dim everything behind it
|
|
934
|
+
- `fullscreen` - full-screen layout without dimming
|
|
935
|
+
- `capture` - while this overlay is registered, key and mouse events are dispatched only to handlers inside the overlay's subtree (the registering component, components rendered in its overlay tree, and overlays they open in turn). Everything else is skipped, except mount-level handlers like the built-in ctrl+c exit. Modal sets this, which is why content behind an open modal never reacts to input regardless of mount order. When several capturing overlays are open, the most recently registered one wins.
|
|
936
|
+
|
|
937
|
+
With neither positioning flag, the overlay is anchored just below the calling component's layout rectangle, which is how Select positions its dropdown.
|
|
938
|
+
|
|
750
939
|
### ScrollableText
|
|
751
940
|
|
|
752
941
|
Used in [explorer](examples/explorer.jsx), [reader](examples/reader.jsx), [highlight](examples/highlight.jsx)
|
|
@@ -760,6 +949,7 @@ Scrollable text viewer. ANSI escape sequences are parsed and rendered, so syntax
|
|
|
760
949
|
scrollOffset={offset} // controlled, or omit for internal state
|
|
761
950
|
onScroll={setOffset}
|
|
762
951
|
scrollbar={true} // default false
|
|
952
|
+
width={60} // wrap width override (default: measured layout width)
|
|
763
953
|
wrap={false} // default true, false truncates long lines
|
|
764
954
|
thumbChar="█" // default █
|
|
765
955
|
trackChar="│" // default │
|
|
@@ -768,6 +958,46 @@ Scrollable text viewer. ANSI escape sequences are parsed and rendered, so syntax
|
|
|
768
958
|
|
|
769
959
|
Keys: same as List (j/k, g/G, ctrl-d/u, ctrl-f/b, pageup/pagedown).
|
|
770
960
|
|
|
961
|
+
### Diff
|
|
962
|
+
|
|
963
|
+
Used in [diff](examples/diff.jsx)
|
|
964
|
+
|
|
965
|
+
Unified diff viewer with line numbers, word-level change highlighting, and optional syntax highlighting. Input is one of three forms: `before`/`after` strings, a unified `patch` string (`git diff` output), or structured `hunks`.
|
|
966
|
+
|
|
967
|
+
```jsx
|
|
968
|
+
<Diff
|
|
969
|
+
before={oldSource} // or patch="diff --git ..." or hunks={[...]}
|
|
970
|
+
after={newSource}
|
|
971
|
+
language="js" // passed to highlight (default 'text')
|
|
972
|
+
filename="src/app.js" // optional header row with +/- stats
|
|
973
|
+
highlight={(code, lang) => ansiString} // optional syntax highlighter
|
|
974
|
+
wordDiff={true} // word-level ranges within changed lines (default true)
|
|
975
|
+
context={3} // context lines around changes, folds the rest (default Infinity)
|
|
976
|
+
lineNumbers={true} // default true
|
|
977
|
+
focused={true}
|
|
978
|
+
scrollOffset={offset} // controlled, or omit for internal state
|
|
979
|
+
onScroll={setOffset}
|
|
980
|
+
scrollbar={true} // default true
|
|
981
|
+
colors={{ addBg: '#10301a' }} // palette overrides
|
|
982
|
+
/>
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
The `highlight` function must be synchronous - the render loop cannot await. For async highlighters like shiki, pre-highlight into a cache before mounting and return cached results (see [diff](examples/diff.jsx)).
|
|
986
|
+
|
|
987
|
+
Keys: j/k or up/down, ctrl-d/u half page, ctrl-f/b or pageup/pagedown full page, g/home for top, G/end for bottom.
|
|
988
|
+
|
|
989
|
+
`computeDiff` is the pure core, exported for building custom diff UIs:
|
|
990
|
+
|
|
991
|
+
```js
|
|
992
|
+
import { computeDiff } from '@trendr/core'
|
|
993
|
+
|
|
994
|
+
const { rows, stats } = computeDiff({ before, after, patch, hunks, wordDiff, context })
|
|
995
|
+
// rows: { type: 'context' | 'add' | 'del' | 'hunk' | 'meta' | 'fold',
|
|
996
|
+
// oldNo, newNo, text, intra }
|
|
997
|
+
// intra is an array of [start, end] changed ranges for word diff
|
|
998
|
+
// stats: { additions, deletions }
|
|
999
|
+
```
|
|
1000
|
+
|
|
771
1001
|
### ScrollBox
|
|
772
1002
|
|
|
773
1003
|
Scrollable container for JSX children (vs ScrollableText which takes a string).
|
|
@@ -829,6 +1059,34 @@ Nesting works:
|
|
|
829
1059
|
</SplitPane>
|
|
830
1060
|
```
|
|
831
1061
|
|
|
1062
|
+
### MillerNav
|
|
1063
|
+
|
|
1064
|
+
Used in [miller-nav](examples/miller-nav.jsx)
|
|
1065
|
+
|
|
1066
|
+
Miller-column navigator in the style of Finder's column view. Columns show the path from root to the active item; children are fetched on demand with a synchronous `getChildren`.
|
|
1067
|
+
|
|
1068
|
+
```jsx
|
|
1069
|
+
<MillerNav
|
|
1070
|
+
rootItems={items}
|
|
1071
|
+
getChildren={item => item.children ?? []} // synchronous, returns an array
|
|
1072
|
+
hasChildren={item => !!item.children} // optional, marks items that can expand
|
|
1073
|
+
onSelectionChange={({ item, breadcrumb, column }) => {}}
|
|
1074
|
+
focused={fm.is('nav')}
|
|
1075
|
+
interactive={true} // handle keyboard input (default: same as focused)
|
|
1076
|
+
scrollbar={false} // default false
|
|
1077
|
+
peekColumn={true} // preview column for the active item's children (default true)
|
|
1078
|
+
maxChars={{ focused: 20, unfocused: 10 }} // column width caps, or a single number
|
|
1079
|
+
divider={true} // vertical divider between columns (default true)
|
|
1080
|
+
dividerChar="▏" // default '▏'
|
|
1081
|
+
dividerColor="#333333" // default '#333333'
|
|
1082
|
+
renderItem={(item, { selected, focused, column }) => <text>{item.name}</text>}
|
|
1083
|
+
/>
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
Items are strings or objects with a `name` or `label` field.
|
|
1087
|
+
|
|
1088
|
+
Keys: j/k or up/down to move within a column, l/right to descend, h/left to go back, ctrl-d/u half page.
|
|
1089
|
+
|
|
832
1090
|
## Animation
|
|
833
1091
|
|
|
834
1092
|
Physics-based animation. Animated values are signals that trigger re-renders.
|
|
@@ -871,20 +1129,22 @@ x.set(newTarget)
|
|
|
871
1129
|
x.onTick((value) => { /* called each frame while animating */ })
|
|
872
1130
|
```
|
|
873
1131
|
|
|
874
|
-
##
|
|
1132
|
+
## Development
|
|
1133
|
+
|
|
1134
|
+
Inside this repo, examples import from the local alias `trend`, which a custom esbuild plugin in `esbuild.config.js` resolves to the local source; published consumers configure `jsxImportSource: '@trendr/core'` as shown in [Usage](#usage).
|
|
875
1135
|
|
|
876
|
-
|
|
1136
|
+
Build the examples:
|
|
877
1137
|
|
|
878
1138
|
```
|
|
879
|
-
|
|
1139
|
+
npm run build
|
|
880
1140
|
```
|
|
881
1141
|
|
|
882
|
-
|
|
1142
|
+
Run one:
|
|
883
1143
|
|
|
884
1144
|
```
|
|
885
|
-
npm run counter
|
|
886
|
-
npm run chat
|
|
887
|
-
npm run
|
|
888
|
-
npm run explorer
|
|
889
|
-
npm run highlight
|
|
1145
|
+
npm run ex counter
|
|
1146
|
+
npm run ex chat
|
|
1147
|
+
npm run ex highlight
|
|
890
1148
|
```
|
|
1149
|
+
|
|
1150
|
+
`npm run ex` without a name lists all available examples.
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { mount } from './src/renderer.js'
|
|
1
|
+
export { mount, registerOverlay } 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 {
|
|
4
|
+
export { 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,15 +16,19 @@ 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 { Diff } from './src/diff-view.js'
|
|
20
|
+
export { computeDiff } from './src/diff-engine.js'
|
|
19
21
|
export { PickList } from './src/pick-list.js'
|
|
20
22
|
export { ScrollBox } from './src/scroll-box.js'
|
|
21
23
|
export { SplitPane } from './src/split-pane.js'
|
|
22
24
|
export { MillerNav } from './src/miller-nav.js'
|
|
23
25
|
export { MenuBar } from './src/menubar.js'
|
|
24
26
|
export { Menu } from './src/menu.js'
|
|
27
|
+
export { Markdown } from './src/markdown.js'
|
|
25
28
|
export { Task } from './src/task.js'
|
|
26
29
|
export { Shimmer } from './src/shimmer.js'
|
|
27
30
|
export { useFocus, useFocusTrap } from './src/focus.js'
|
|
31
|
+
export { useSelection, extractSelectionText } from './src/selection.js'
|
|
28
32
|
export { useHotkey } from './src/hotkey.js'
|
|
29
33
|
export { useToast } from './src/toast.js'
|
|
30
34
|
export { Fragment } from './src/element.js'
|
package/package.json
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trendr/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "direct-mode TUI renderer with JSX, signals, and per-cell diffing",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": "Jonathan Pyers",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"main": "./index.js",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/nvms/trendr.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/nvms/trendr/issues"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/nvms/trendr#readme",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"tui",
|
|
20
|
+
"terminal",
|
|
21
|
+
"renderer",
|
|
22
|
+
"jsx",
|
|
23
|
+
"signals",
|
|
24
|
+
"cli",
|
|
25
|
+
"ansi",
|
|
26
|
+
"flexbox",
|
|
27
|
+
"console",
|
|
28
|
+
"components"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
8
33
|
"files": [
|
|
9
34
|
"src",
|
|
10
35
|
"index.js",
|
|
@@ -16,40 +41,10 @@
|
|
|
16
41
|
"./jsx-dev-runtime": "./jsx-runtime.js"
|
|
17
42
|
},
|
|
18
43
|
"scripts": {
|
|
19
|
-
"test": "node test/test.js && node test/test-e2e.js && node test/test-mount.js && node test/test-render.js",
|
|
44
|
+
"test": "node test/test.js && node test/test-e2e.js && node test/test-mount.js && node test/test-render.js && node test/test-diff.js && node test/test-input.js && node test/test-components.js",
|
|
20
45
|
"build": "node esbuild.config.js",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
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
|
-
"multi-row-list": "node esbuild.config.js && node dist/multi-row-list.js",
|
|
32
|
-
"plasma": "node esbuild.config.js && node dist/plasma.js",
|
|
33
|
-
"rain": "node esbuild.config.js && node dist/rain.js",
|
|
34
|
-
"animation": "node esbuild.config.js && node dist/animation.js",
|
|
35
|
-
"texture": "node esbuild.config.js && node dist/texture.js",
|
|
36
|
-
"absolute": "node esbuild.config.js && node dist/absolute.js",
|
|
37
|
-
"scroll-box": "node esbuild.config.js && node dist/scroll-box.js",
|
|
38
|
-
"split-pane": "node esbuild.config.js && node dist/split-pane.js",
|
|
39
|
-
"custom-table": "node esbuild.config.js && node dist/custom-table.js",
|
|
40
|
-
"async": "node esbuild.config.js && node dist/async.js",
|
|
41
|
-
"timeout": "node esbuild.config.js && node dist/timeout.js",
|
|
42
|
-
"task": "node esbuild.config.js && node dist/task.js",
|
|
43
|
-
"progress": "node esbuild.config.js && node dist/progress.js",
|
|
44
|
-
"shimmer": "node esbuild.config.js && node dist/shimmer.js",
|
|
45
|
-
"spinner": "node esbuild.config.js && node dist/spinner.js",
|
|
46
|
-
"miller-nav": "node esbuild.config.js && node dist/miller-nav.js",
|
|
47
|
-
"pick-list": "node esbuild.config.js && node dist/pick-list.js",
|
|
48
|
-
"table-demo": "node esbuild.config.js && node dist/table-demo.js",
|
|
49
|
-
"menubar": "node esbuild.config.js && node dist/menubar.js",
|
|
50
|
-
"inline-chat": "node esbuild.config.js && node dist/inline-chat.js",
|
|
51
|
-
"demo": "node esbuild.config.js && node demo/server.js",
|
|
52
|
-
"demo:build": "node esbuild.config.js"
|
|
46
|
+
"ex": "node scripts/run-example.js",
|
|
47
|
+
"demo": "node esbuild.config.js && node demo/server.js"
|
|
53
48
|
},
|
|
54
49
|
"devDependencies": {
|
|
55
50
|
"esbuild": "^0.25.0",
|