Skip to content

Commit aeef2b7

Browse files
authored
v0.3.19: openai oss models, invite & search modal fixes
2 parents 2bba201 + 6ec5cf4 commit aeef2b7

File tree

4 files changed

+750
-541
lines changed

4 files changed

+750
-541
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { useCallback, useEffect, useRef, useState } from 'react'
2+
3+
export interface NavigationSection {
4+
id: string
5+
name: string
6+
type: 'grid' | 'list'
7+
items: any[]
8+
gridCols?: number // How many columns per row for grid sections
9+
}
10+
11+
export interface NavigationPosition {
12+
sectionIndex: number
13+
itemIndex: number
14+
}
15+
16+
export function useSearchNavigation(sections: NavigationSection[], isOpen: boolean) {
17+
const [position, setPosition] = useState<NavigationPosition>({ sectionIndex: 0, itemIndex: 0 })
18+
const scrollRefs = useRef<Map<string, HTMLElement>>(new Map())
19+
const lastPositionInSection = useRef<Map<string, number>>(new Map())
20+
21+
// Reset position when sections change or modal opens
22+
useEffect(() => {
23+
if (sections.length > 0) {
24+
setPosition({ sectionIndex: 0, itemIndex: 0 })
25+
}
26+
}, [sections, isOpen])
27+
28+
const getCurrentItem = useCallback(() => {
29+
if (sections.length === 0 || position.sectionIndex >= sections.length) return null
30+
31+
const section = sections[position.sectionIndex]
32+
if (position.itemIndex >= section.items.length) return null
33+
34+
return {
35+
section,
36+
item: section.items[position.itemIndex],
37+
position,
38+
}
39+
}, [sections, position])
40+
41+
const navigate = useCallback(
42+
(direction: 'up' | 'down' | 'left' | 'right') => {
43+
if (sections.length === 0) return
44+
45+
const currentSection = sections[position.sectionIndex]
46+
if (!currentSection) return
47+
48+
const isGridSection = currentSection.type === 'grid'
49+
const gridCols = currentSection.gridCols || 1
50+
51+
setPosition((prevPosition) => {
52+
let newSectionIndex = prevPosition.sectionIndex
53+
let newItemIndex = prevPosition.itemIndex
54+
55+
if (direction === 'up') {
56+
if (isGridSection) {
57+
// In grid: up moves to previous row in same section, or previous section
58+
if (newItemIndex >= gridCols) {
59+
newItemIndex -= gridCols
60+
} else if (newSectionIndex > 0) {
61+
// Save current position before moving to previous section
62+
lastPositionInSection.current.set(currentSection.id, newItemIndex)
63+
64+
// Move to previous section
65+
newSectionIndex -= 1
66+
const prevSection = sections[newSectionIndex]
67+
68+
// Restore last position in that section, or go to end
69+
const lastPos = lastPositionInSection.current.get(prevSection.id)
70+
if (lastPos !== undefined && lastPos < prevSection.items.length) {
71+
newItemIndex = lastPos
72+
} else {
73+
newItemIndex = Math.max(0, prevSection.items.length - 1)
74+
}
75+
}
76+
} else {
77+
// In list: up moves to previous item, or previous section
78+
if (newItemIndex > 0) {
79+
newItemIndex -= 1
80+
} else if (newSectionIndex > 0) {
81+
// Save current position before moving to previous section
82+
lastPositionInSection.current.set(currentSection.id, newItemIndex)
83+
84+
newSectionIndex -= 1
85+
const prevSection = sections[newSectionIndex]
86+
87+
// Restore last position in that section, or go to end
88+
const lastPos = lastPositionInSection.current.get(prevSection.id)
89+
if (lastPos !== undefined && lastPos < prevSection.items.length) {
90+
newItemIndex = lastPos
91+
} else {
92+
newItemIndex = Math.max(0, prevSection.items.length - 1)
93+
}
94+
}
95+
}
96+
} else if (direction === 'down') {
97+
if (isGridSection) {
98+
// In grid: down moves to next row in same section, or next section
99+
const maxIndexInCurrentRow = Math.min(
100+
newItemIndex + gridCols,
101+
currentSection.items.length - 1
102+
)
103+
104+
if (newItemIndex + gridCols < currentSection.items.length) {
105+
newItemIndex += gridCols
106+
} else if (newSectionIndex < sections.length - 1) {
107+
// Save current position before moving to next section
108+
lastPositionInSection.current.set(currentSection.id, newItemIndex)
109+
110+
// Move to next section
111+
newSectionIndex += 1
112+
const nextSection = sections[newSectionIndex]
113+
114+
// Restore last position in next section, or start at beginning
115+
const lastPos = lastPositionInSection.current.get(nextSection.id)
116+
if (lastPos !== undefined && lastPos < nextSection.items.length) {
117+
newItemIndex = lastPos
118+
} else {
119+
newItemIndex = 0
120+
}
121+
}
122+
} else {
123+
// In list: down moves to next item, or next section
124+
if (newItemIndex < currentSection.items.length - 1) {
125+
newItemIndex += 1
126+
} else if (newSectionIndex < sections.length - 1) {
127+
// Save current position before moving to next section
128+
lastPositionInSection.current.set(currentSection.id, newItemIndex)
129+
130+
newSectionIndex += 1
131+
const nextSection = sections[newSectionIndex]
132+
133+
// Restore last position in next section, or start at beginning
134+
const lastPos = lastPositionInSection.current.get(nextSection.id)
135+
if (lastPos !== undefined && lastPos < nextSection.items.length) {
136+
newItemIndex = lastPos
137+
} else {
138+
newItemIndex = 0
139+
}
140+
}
141+
}
142+
} else if (direction === 'left' && isGridSection) {
143+
// In grid: left moves to previous item in same row
144+
if (newItemIndex > 0) {
145+
const currentRow = Math.floor(newItemIndex / gridCols)
146+
const newIndex = newItemIndex - 1
147+
const newRow = Math.floor(newIndex / gridCols)
148+
149+
// Only move if we stay in the same row
150+
if (currentRow === newRow) {
151+
newItemIndex = newIndex
152+
}
153+
}
154+
} else if (direction === 'right' && isGridSection) {
155+
// In grid: right moves to next item in same row
156+
if (newItemIndex < currentSection.items.length - 1) {
157+
const currentRow = Math.floor(newItemIndex / gridCols)
158+
const newIndex = newItemIndex + 1
159+
const newRow = Math.floor(newIndex / gridCols)
160+
161+
// Only move if we stay in the same row
162+
if (currentRow === newRow) {
163+
newItemIndex = newIndex
164+
}
165+
}
166+
}
167+
168+
return { sectionIndex: newSectionIndex, itemIndex: newItemIndex }
169+
})
170+
},
171+
[sections, position]
172+
)
173+
174+
// Scroll selected item into view
175+
useEffect(() => {
176+
const current = getCurrentItem()
177+
if (!current) return
178+
179+
const { section, position: currentPos } = current
180+
const scrollContainer = scrollRefs.current.get(section.id)
181+
182+
if (scrollContainer) {
183+
const itemElement = scrollContainer.querySelector(
184+
`[data-nav-item="${section.id}-${currentPos.itemIndex}"]`
185+
) as HTMLElement
186+
187+
if (itemElement) {
188+
// For horizontal scrolling sections (blocks/tools)
189+
if (section.type === 'grid') {
190+
const containerRect = scrollContainer.getBoundingClientRect()
191+
const itemRect = itemElement.getBoundingClientRect()
192+
193+
// Check if item is outside the visible area horizontally
194+
if (itemRect.left < containerRect.left) {
195+
scrollContainer.scrollTo({
196+
left: scrollContainer.scrollLeft - (containerRect.left - itemRect.left + 20),
197+
behavior: 'smooth',
198+
})
199+
} else if (itemRect.right > containerRect.right) {
200+
scrollContainer.scrollTo({
201+
left: scrollContainer.scrollLeft + (itemRect.right - containerRect.right + 20),
202+
behavior: 'smooth',
203+
})
204+
}
205+
}
206+
207+
// Always ensure vertical visibility
208+
itemElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
209+
}
210+
}
211+
}, [getCurrentItem, position])
212+
213+
return {
214+
navigate,
215+
getCurrentItem,
216+
scrollRefs,
217+
position,
218+
}
219+
}

0 commit comments

Comments
 (0)