@trendr/core 0.1.0 → 0.2.0

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