@trendr/core 0.2.11 → 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 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
- 4-16x faster frame times and 580x less I/O per render than popular TUI frameworks. No dependencies. [benchmarks](bench/README.md)
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": "trend" }
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, { stream?, stdin?, title?, theme? })` enters alt screen and returns `{ unmount, repaint }`. Renders on demand when signals change, capped at 60fps.
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} // default 10
426
- onChange={v => {}} // every edit
427
- onSubmit={v => {}} // Alt+Enter
428
- onCancel={() => {}} // Escape
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
- onSelect={item => {}} // Enter on highlighted item
585
+ onSubmit={item => {}} // Enter on highlighted item
487
586
  onCancel={() => {}} // Escape
488
587
  onChange={query => {}} // every keystroke in the filter
489
- clearOnSelect={false} // reset filter on select (default false)
588
+ clearOnSubmit={false} // reset filter on submit (default false)
490
589
  scrollbar={true} // default false
491
- scrolloff={2} // items of margin from edges (default 2, inherited from List)
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
- onSelect={setTab}
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
- onSelect={setColor}
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
- onSelect={setSize}
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} // default 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
- ## Build
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
- Uses esbuild. JSX configured with `jsxImportSource: 'trend'`.
1136
+ Build the examples:
877
1137
 
878
1138
  ```
879
- node esbuild.config.js
1139
+ npm run build
880
1140
  ```
881
1141
 
882
- Examples run via npm scripts:
1142
+ Run one:
883
1143
 
884
1144
  ```
885
- npm run counter
886
- npm run chat
887
- npm run dashboard
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 { useState, useInput, useMouse, useResize, useInterval, useTimeout, useLayout, useStdout, useRepaint, useTitle, useTheme, useCursor, useFrameStats, useAsync } from './src/hooks.js'
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.2.11",
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
- "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
- "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",