1
+ import { useEditorEngine } from '@/components/store/editor' ;
2
+ import { LeftPanelTabValue , type PageNode , type WebFrame } from '@onlook/models' ;
3
+ import { Button } from '@onlook/ui/button' ;
4
+ import {
5
+ DropdownMenu ,
6
+ DropdownMenuContent ,
7
+ DropdownMenuItem ,
8
+ DropdownMenuTrigger ,
9
+ } from '@onlook/ui/dropdown-menu' ;
10
+ import { Icons } from '@onlook/ui/icons' ;
11
+ import { Separator } from '@onlook/ui/separator' ;
12
+ import { cn } from '@onlook/ui/utils' ;
13
+ import { inferPageFromUrl } from '@onlook/utility' ;
14
+ import { observer } from 'mobx-react-lite' ;
15
+ import React , { useEffect , useMemo , useState } from 'react' ;
16
+ import { PageModal } from '../../left-panel/page-tab/page-modal' ;
17
+
18
+ interface PageSelectorProps {
19
+ frame : WebFrame ;
20
+ className ?: string ;
21
+ }
22
+
23
+ export const PageSelector = observer ( ( { frame, className } : PageSelectorProps ) => {
24
+ const editorEngine = useEditorEngine ( ) ;
25
+ const [ showCreateModal , setShowCreateModal ] = useState ( false ) ;
26
+
27
+ // Get inferred current page from URL immediately
28
+ const inferredCurrentPage = useMemo ( ( ) => inferPageFromUrl ( frame . url ) , [ frame . url ] ) ;
29
+
30
+ // Flatten the page tree to get all pages for finding current page
31
+ const flattenPages = ( pages : PageNode [ ] ) : PageNode [ ] => {
32
+ return pages . reduce < PageNode [ ] > ( ( acc , page ) => {
33
+ acc . push ( page ) ;
34
+ if ( page . children ) {
35
+ acc . push ( ...flattenPages ( page . children ) ) ;
36
+ }
37
+ return acc ;
38
+ } , [ ] ) ;
39
+ } ;
40
+
41
+ const allPages = useMemo ( ( ) => {
42
+ return flattenPages ( editorEngine . pages . tree ) ;
43
+ } , [ editorEngine . pages . tree ] ) ;
44
+
45
+ // Find the current page based on the frame URL
46
+ const currentPage = useMemo ( ( ) => {
47
+ const framePathname = new URL ( frame . url ) . pathname ;
48
+ return allPages . find ( page => {
49
+ const pagePath = page . path === '/' ? '' : page . path ;
50
+ return framePathname === pagePath || framePathname === page . path ;
51
+ } ) ;
52
+ } , [ frame . url , allPages ] ) ;
53
+
54
+ // Render pages recursively with indentation
55
+ const renderPageItems = ( pages : PageNode [ ] , depth = 0 ) : React . ReactElement [ ] => {
56
+ const items : React . ReactElement [ ] = [ ] ;
57
+
58
+ for ( const page of pages ) {
59
+ const isCurrentPage = currentPage ?. id === page . id ;
60
+ const hasChildren = page . children && page . children . length > 0 ;
61
+
62
+ items . push (
63
+ < DropdownMenuItem
64
+ key = { page . id }
65
+ onClick = { ( ) => handlePageSelect ( page ) }
66
+ className = { cn (
67
+ "cursor-pointer" ,
68
+ isCurrentPage && "bg-accent"
69
+ ) }
70
+ >
71
+ < div className = "flex items-center w-full" style = { { paddingLeft : `${ depth * 16 } px` } } >
72
+ { hasChildren ? (
73
+ < Icons . Directory className = "w-4 h-4 mr-2" />
74
+ ) : (
75
+ < Icons . File className = "w-4 h-4 mr-2" />
76
+ ) }
77
+ < span className = "truncate" > { page . name } </ span >
78
+ { isCurrentPage && (
79
+ < Icons . Check className = "ml-auto h-3 w-3" />
80
+ ) }
81
+ </ div >
82
+ </ DropdownMenuItem >
83
+ ) ;
84
+
85
+ // Render children recursively
86
+ if ( page . children && page . children . length > 0 ) {
87
+ items . push ( ...renderPageItems ( page . children , depth + 1 ) ) ;
88
+ }
89
+ }
90
+
91
+ return items ;
92
+ } ;
93
+
94
+ useEffect ( ( ) => {
95
+ if ( editorEngine . sandbox . routerConfig ) {
96
+ editorEngine . pages . scanPages ( ) ;
97
+ }
98
+ } , [ editorEngine . sandbox . routerConfig ] ) ;
99
+
100
+ const displayPages = useMemo ( ( ) => {
101
+ if ( allPages . length > 0 ) {
102
+ return allPages ;
103
+ }
104
+ // Temp page while scanning
105
+ return [ {
106
+ id : 'temp-current' ,
107
+ name : inferredCurrentPage . name ,
108
+ path : inferredCurrentPage . path ,
109
+ children : [ ] ,
110
+ isActive : true ,
111
+ isRoot : inferredCurrentPage . path === '/' ,
112
+ metadata : { }
113
+ } ] as PageNode [ ] ;
114
+ } , [ allPages , inferredCurrentPage ] ) ;
115
+
116
+ const displayCurrentPage = currentPage ?? {
117
+ name : inferredCurrentPage . name ,
118
+ path : inferredCurrentPage . path
119
+ } ;
120
+
121
+ const handlePageSelect = async ( page : PageNode ) => {
122
+ try {
123
+ await editorEngine . frames . navigateToPath ( frame . id , page . path ) ;
124
+ } catch ( error ) {
125
+ console . error ( 'Failed to navigate to page:' , error ) ;
126
+ }
127
+ } ;
128
+
129
+ const handleManagePages = ( ) => {
130
+ editorEngine . state . leftPanelTab = LeftPanelTabValue . PAGES
131
+ editorEngine . state . leftPanelLocked = true ;
132
+ } ;
133
+
134
+ return (
135
+ < DropdownMenu onOpenChange = { ( open ) => {
136
+ if ( open ) {
137
+ editorEngine . frames . select ( [ frame ] ) ;
138
+ }
139
+ } } >
140
+ < DropdownMenuTrigger asChild >
141
+ < Button
142
+ variant = "ghost"
143
+ size = "sm"
144
+ className = { cn (
145
+ "h-auto px-2 py-1 text-xs hover:bg-background-secondary" ,
146
+ className
147
+ ) }
148
+ >
149
+ < span className = "max-w-32 truncate" >
150
+ { displayCurrentPage . name }
151
+ </ span >
152
+ < Icons . ChevronDown className = "ml-1 h-3 w-3" />
153
+ </ Button >
154
+ </ DropdownMenuTrigger >
155
+ < DropdownMenuContent align = "start" className = "w-48" >
156
+ { displayPages . length > 0 ? (
157
+ < >
158
+ { allPages . length > 0 ? (
159
+ // Show full scanned tree when available
160
+ renderPageItems ( editorEngine . pages . tree )
161
+ ) : (
162
+ // Show inferred current page while scanning
163
+ < >
164
+ { displayPages [ 0 ] && (
165
+ < DropdownMenuItem
166
+ onClick = { ( ) => {
167
+ const firstPage = displayPages [ 0 ] ;
168
+ if ( firstPage ) {
169
+ void handlePageSelect ( firstPage ) ;
170
+ }
171
+ } }
172
+ className = "cursor-pointer bg-accent"
173
+ >
174
+ < div className = "flex items-center w-full" >
175
+ < Icons . File className = "w-4 h-4 mr-2" />
176
+ < span className = "truncate" > { displayPages [ 0 ] . name } </ span >
177
+ < Icons . Check className = "ml-auto h-3 w-3" />
178
+ </ div >
179
+ </ DropdownMenuItem >
180
+ ) }
181
+ { editorEngine . pages . isScanning && (
182
+ < DropdownMenuItem disabled className = "text-xs text-muted-foreground" >
183
+ < Icons . LoadingSpinner className = "w-3 h-3 mr-2 animate-spin" />
184
+ < span > Scanning pages...</ span >
185
+ </ DropdownMenuItem >
186
+ ) }
187
+ </ >
188
+ ) }
189
+ </ >
190
+ ) : (
191
+ < DropdownMenuItem disabled >
192
+ No pages available
193
+ </ DropdownMenuItem >
194
+ ) }
195
+ < Separator />
196
+ < DropdownMenuItem className = "cursor-pointer " onClick = { ( ) => setShowCreateModal ( true ) } >
197
+ < Icons . FilePlus />
198
+ < span >
199
+ New Page
200
+ </ span >
201
+ </ DropdownMenuItem >
202
+ < DropdownMenuItem className = "cursor-pointer" onClick = { handleManagePages } >
203
+ < Icons . Gear />
204
+ < span >
205
+ Manage Pages
206
+ </ span >
207
+ </ DropdownMenuItem >
208
+ </ DropdownMenuContent >
209
+ < PageModal mode = "create" open = { showCreateModal } onOpenChange = { setShowCreateModal } />
210
+
211
+ </ DropdownMenu >
212
+ ) ;
213
+ } ) ;
0 commit comments