@effect-tui/react 0.5.0 → 0.6.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/dist/src/components/ListView.d.ts +58 -0
- package/dist/src/components/ListView.d.ts.map +1 -0
- package/dist/src/components/ListView.js +68 -0
- package/dist/src/components/ListView.js.map +1 -0
- package/dist/src/components/index.d.ts +1 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/dist/src/components/index.js +1 -0
- package/dist/src/components/index.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/ListView.tsx +112 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "React bindings for @effect-tui/core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"prepublishOnly": "bun run typecheck && bun run build"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@effect-tui/core": "^0.
|
|
86
|
+
"@effect-tui/core": "^0.6.0",
|
|
87
87
|
"@effect/platform": "^0.94.0",
|
|
88
88
|
"@effect/platform-bun": "^0.87.0",
|
|
89
89
|
"@effect/rpc": "^0.73.0",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// ListView.tsx — Selection-aware scrolling list with scroll-into-view behavior
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
import { useEffect, useRef } from "react"
|
|
4
|
+
import { useScroll } from "../hooks/use-scroll.js"
|
|
5
|
+
|
|
6
|
+
export interface ListViewProps<T> {
|
|
7
|
+
/** Items to render */
|
|
8
|
+
items: readonly T[]
|
|
9
|
+
/** Currently selected index */
|
|
10
|
+
selectedIndex: number
|
|
11
|
+
/** Render function for each item */
|
|
12
|
+
renderItem: (item: T, index: number, isSelected: boolean) => ReactNode
|
|
13
|
+
/** Extract unique key for each item */
|
|
14
|
+
keyExtractor: (item: T, index: number) => string | number
|
|
15
|
+
/** Height of each item in rows (default: 1) */
|
|
16
|
+
itemHeight?: number
|
|
17
|
+
/** Padding rows to keep around selected item (default: 0) */
|
|
18
|
+
scrollPadding?: number
|
|
19
|
+
/** Show scrollbar (default: true) */
|
|
20
|
+
showScrollbar?: boolean
|
|
21
|
+
/** Empty state content when items is empty */
|
|
22
|
+
emptyContent?: ReactNode
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A scrolling list view with selection and scroll-into-view behavior.
|
|
27
|
+
*
|
|
28
|
+
* Features:
|
|
29
|
+
* - Automatically scrolls to keep selected item visible when selection changes
|
|
30
|
+
* - Supports mouse wheel scrolling with macOS-style acceleration
|
|
31
|
+
* - Shows a scroll bar when content overflows
|
|
32
|
+
*
|
|
33
|
+
* Keyboard navigation should be handled by the parent component - update
|
|
34
|
+
* `selectedIndex` and ListView will scroll to keep it visible.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* const [selectedIndex, setSelectedIndex] = useState(0)
|
|
39
|
+
*
|
|
40
|
+
* // Handle keyboard in parent
|
|
41
|
+
* useKeyboard((key) => {
|
|
42
|
+
* if (key.name === "down" || key.text === "j") {
|
|
43
|
+
* setSelectedIndex(i => Math.min(items.length - 1, i + 1))
|
|
44
|
+
* }
|
|
45
|
+
* if (key.name === "up" || key.text === "k") {
|
|
46
|
+
* setSelectedIndex(i => Math.max(0, i - 1))
|
|
47
|
+
* }
|
|
48
|
+
* })
|
|
49
|
+
*
|
|
50
|
+
* return (
|
|
51
|
+
* <ListView
|
|
52
|
+
* items={cards}
|
|
53
|
+
* selectedIndex={selectedIndex}
|
|
54
|
+
* renderItem={(card, i, isSelected) => (
|
|
55
|
+
* <CardListItem card={card} isSelected={isSelected} />
|
|
56
|
+
* )}
|
|
57
|
+
* keyExtractor={(card) => card.id}
|
|
58
|
+
* />
|
|
59
|
+
* )
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function ListView<T>({
|
|
63
|
+
items,
|
|
64
|
+
selectedIndex,
|
|
65
|
+
renderItem,
|
|
66
|
+
keyExtractor,
|
|
67
|
+
itemHeight = 1,
|
|
68
|
+
scrollPadding = 0,
|
|
69
|
+
showScrollbar = true,
|
|
70
|
+
emptyContent,
|
|
71
|
+
}: ListViewProps<T>) {
|
|
72
|
+
const { scrollProps, scrollToVisible } = useScroll({
|
|
73
|
+
enableKeyboard: false, // Parent handles keyboard for selection
|
|
74
|
+
enableMouseWheel: true, // Free scroll with wheel
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Track previous selection to detect changes
|
|
78
|
+
const prevSelectedRef = useRef(selectedIndex)
|
|
79
|
+
|
|
80
|
+
// Scroll to keep selection visible when it changes
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (selectedIndex !== prevSelectedRef.current) {
|
|
83
|
+
prevSelectedRef.current = selectedIndex
|
|
84
|
+
scrollToVisible(selectedIndex, itemHeight, scrollPadding)
|
|
85
|
+
}
|
|
86
|
+
}, [selectedIndex, itemHeight, scrollPadding, scrollToVisible])
|
|
87
|
+
|
|
88
|
+
// Also scroll on initial render if selection is not 0
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (selectedIndex > 0) {
|
|
91
|
+
scrollToVisible(selectedIndex, itemHeight, scrollPadding)
|
|
92
|
+
}
|
|
93
|
+
// Only run on mount
|
|
94
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
95
|
+
}, [])
|
|
96
|
+
|
|
97
|
+
if (items.length === 0 && emptyContent) {
|
|
98
|
+
return <>{emptyContent}</>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<scroll {...scrollProps} showScrollbar={showScrollbar}>
|
|
103
|
+
<vstack>
|
|
104
|
+
{items.map((item, index) => (
|
|
105
|
+
<vstack key={keyExtractor(item, index)}>
|
|
106
|
+
{renderItem(item, index, index === selectedIndex)}
|
|
107
|
+
</vstack>
|
|
108
|
+
))}
|
|
109
|
+
</vstack>
|
|
110
|
+
</scroll>
|
|
111
|
+
)
|
|
112
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Divider, type DividerProps } from "./Divider.js"
|
|
2
|
+
export { ListView, type ListViewProps } from "./ListView.js"
|
|
2
3
|
export { Markdown, type MarkdownProps, type MarkdownTheme } from "./Markdown.js"
|
|
3
4
|
export { MultilineTextInput, type MultilineTextInputProps } from "./MultilineTextInput.js"
|
|
4
5
|
export { Overlay, type OverlayItemProps, type OverlayProps } from "./Overlay.js"
|