forked from CodebuffAI/codebuff
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsuggestion-menu.tsx
More file actions
207 lines (190 loc) · 6.14 KB
/
Copy pathsuggestion-menu.tsx
File metadata and controls
207 lines (190 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
import React, { useEffect, useState } from 'react'
import { Button } from './button'
import { HighlightedSubsequenceText } from './highlighted-text'
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
import { useTheme } from '../hooks/use-theme'
export interface SuggestionItem {
id: string
label: string
labelHighlightIndices?: number[] | null
description: string
descriptionHighlightIndices?: number[] | null
}
interface SuggestionMenuProps {
items: SuggestionItem[]
selectedIndex: number
maxVisible: number
prefix?: string
onItemClick?: (index: number) => void
}
export const SuggestionMenu = ({
items,
selectedIndex,
maxVisible,
prefix = '/',
onItemClick,
}: SuggestionMenuProps) => {
const theme = useTheme()
const { terminalWidth } = useTerminalDimensions()
const screenPadding = 4
const menuWidth = Math.max(10, terminalWidth - screenPadding * 2)
// Hover state: only highlight on hover after user has moved mouse
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
const [hasHoveredSinceOpen, setHasHoveredSinceOpen] = useState(false)
// Reset hover state when items change (new menu session)
useEffect(() => {
setHasHoveredSinceOpen(false)
setHoveredIndex(null)
}, [items])
if (items.length === 0) {
return null
}
const effectivePrefix = prefix ?? ''
const clampedSelected = Math.min(
Math.max(selectedIndex, 0),
Math.max(items.length - 1, 0),
)
const visibleCount = Math.min(Math.max(maxVisible, 1), items.length)
const maxStart = Math.max(items.length - visibleCount, 0)
const idealStart = clampedSelected - Math.floor((visibleCount - 1) / 2)
const start = Math.max(0, Math.min(idealStart, maxStart))
const visibleItems = items.slice(start, start + visibleCount)
// Calculate max label length for alignment
const maxLabelLength = Math.max(
...visibleItems.map(
(item) => effectivePrefix.length + item.label.length,
),
)
// Find the longest description to determine if we can use same-line layout
const maxDescriptionLength = Math.max(
...visibleItems.map((item) => item.description.length),
)
// Check if all items can fit on same line with aligned descriptions
const minWidthForSameLine = maxLabelLength + 2 + maxDescriptionLength
const useSameLine = menuWidth >= minWidthForSameLine
const renderSuggestionItem = (item: SuggestionItem, idx: number) => {
const absoluteIndex = start + idx
const isSelected = absoluteIndex === clampedSelected
const isHovered = hasHoveredSinceOpen && absoluteIndex === hoveredIndex
const isHighlighted = isSelected || isHovered
const labelLength = effectivePrefix.length + item.label.length
const textColor = isHighlighted ? theme.foreground : theme.inputFg
const descriptionColor = isHighlighted ? theme.foreground : theme.muted
const highlightColor = theme.primary
const handleClick = onItemClick ? () => onItemClick(absoluteIndex) : undefined
const handleMouseOver = () => {
setHoveredIndex(absoluteIndex)
setHasHoveredSinceOpen(true)
}
if (useSameLine) {
// Calculate padding to align descriptions
const paddingLength = maxLabelLength - labelLength
const padding = ' '.repeat(paddingLength)
// Wide terminal: description on same line with 2-space gap
return (
<Button
key={item.id}
onClick={handleClick}
onMouseOver={handleMouseOver}
style={{
flexDirection: 'column',
gap: 0,
paddingLeft: 1,
paddingRight: 1,
paddingTop: 0,
paddingBottom: 0,
backgroundColor: isHighlighted ? theme.surfaceHover : theme.background,
width: '100%',
}}
>
<text
style={{
fg: textColor,
marginBottom: 0,
}}
>
<span fg={theme.primary}>{effectivePrefix}</span>
<HighlightedSubsequenceText
text={item.label}
indices={item.labelHighlightIndices}
color={textColor}
highlightColor={highlightColor}
/>
<span>{padding} </span>
<HighlightedSubsequenceText
text={item.description}
indices={item.descriptionHighlightIndices}
color={descriptionColor}
highlightColor={highlightColor}
/>
</text>
</Button>
)
} else {
// Narrow terminal: description on next line
return (
<Button
key={item.id}
onClick={handleClick}
onMouseOver={handleMouseOver}
style={{
flexDirection: 'column',
gap: 0,
paddingLeft: 1,
paddingRight: 1,
paddingTop: 0,
paddingBottom: 0,
backgroundColor: isHighlighted ? theme.surfaceHover : theme.background,
width: '100%',
}}
>
<text
style={{
fg: textColor,
marginBottom: 0,
}}
>
<span fg={theme.primary}>{effectivePrefix}</span>
<HighlightedSubsequenceText
text={item.label}
indices={item.labelHighlightIndices}
color={textColor}
highlightColor={highlightColor}
/>
</text>
<text
style={{
fg: descriptionColor,
marginBottom: 0,
marginLeft: 2,
}}
>
<HighlightedSubsequenceText
text={item.description}
indices={item.descriptionHighlightIndices}
color={descriptionColor}
highlightColor={highlightColor}
/>
</text>
</Button>
)
}
}
return (
<box
style={{
flexDirection: 'column',
gap: 0,
paddingLeft: 0,
paddingRight: 0,
paddingTop: 0,
paddingBottom: 0,
backgroundColor: theme.surface,
width: '100%',
}}
onMouseOut={() => setHoveredIndex(null)}
>
{visibleItems.map(renderSuggestionItem)}
</box>
)
}