1- import { onMount , onCleanup } from "solid-js" ;
1+ import { createSignal , onMount , onCleanup } from "solid-js" ;
22import { appStore } from "../../stores/app-store" ;
33import * as ipc from "../../ipc" ;
44
5- // Track created webviews
65const created = new Set < string > ( ) ;
76
87interface Props {
98 threadId : string ;
109}
1110
12- /**
13- * This component is JUST a placeholder div. The native webview
14- * is positioned over it by Rust. All controls (URL bar, buttons)
15- * live in the TabBar component above — NOT here — because the
16- * native webview renders on top of all DOM content.
17- */
1811export function BrowserPanel ( props : Props ) {
19- const { store } = appStore ;
20- let ref : HTMLDivElement | undefined ;
12+ const { store, setStore } = appStore ;
13+ const [ urlInput , setUrlInput ] = createSignal (
14+ store . threadBrowserUrls [ props . threadId ] || "https://google.com"
15+ ) ;
16+ let viewportRef : HTMLDivElement | undefined ;
17+ let barRef : HTMLDivElement | undefined ;
2118 let lastKey = "" ;
2219
20+ // The native webview is positioned relative to the OS window content area.
21+ // getBoundingClientRect() gives coords relative to the main webview viewport.
22+ // There's a small offset (~5px) between these two coordinate systems.
23+ // We also need to add the address bar height since the webview must sit below it.
24+ const Y_OFFSET = 25 ;
25+
26+ function getBounds ( ) {
27+ if ( ! viewportRef ) return null ;
28+ const r = viewportRef . getBoundingClientRect ( ) ;
29+ if ( r . width < 10 || r . height < 10 ) return null ;
30+ const barH = barRef ? barRef . getBoundingClientRect ( ) . height : 0 ;
31+ return {
32+ x : Math . round ( r . left ) ,
33+ y : Math . round ( r . top + Y_OFFSET + barH ) ,
34+ w : Math . round ( r . width ) ,
35+ h : Math . round ( r . height - barH ) ,
36+ } ;
37+ }
38+
2339 function sync ( ) {
24- if ( ! ref ) return ;
25- const r = ref . getBoundingClientRect ( ) ;
26- if ( r . width < 10 || r . height < 10 ) return ;
27- const key = `${ Math . round ( r . left ) } ,${ Math . round ( r . top ) } ,${ Math . round ( r . width ) } ,${ Math . round ( r . height ) } ` ;
40+ const b = getBounds ( ) ;
41+ if ( ! b || b . h < 10 ) return ;
42+ const key = `${ b . x } ,${ b . y } ,${ b . w } ,${ b . h } ` ;
2843 if ( key === lastKey ) return ;
2944 lastKey = key ;
30- ipc . browserSetBounds ( props . threadId , Math . round ( r . left ) , Math . round ( r . top ) , Math . round ( r . width ) , Math . round ( r . height ) ) . catch ( ( ) => { } ) ;
45+ ipc . browserSetBounds ( props . threadId , b . x , b . y , b . w , b . h ) . catch ( ( ) => { } ) ;
3146 }
3247
3348 onMount ( ( ) => {
3449 const t = setTimeout ( ( ) => {
35- if ( ! ref ) return ;
36- const r = ref . getBoundingClientRect ( ) ;
37- if ( r . width < 10 || r . height < 10 ) return ;
38- const url = store . threadBrowserUrls [ props . threadId ] || "https://google.com" ;
50+ const b = getBounds ( ) ;
51+ if ( ! b || b . h < 10 ) return ;
52+ const url = urlInput ( ) ;
3953
4054 if ( created . has ( props . threadId ) ) {
41- // Already exists — just reposition
42- ipc . browserSetBounds ( props . threadId , Math . round ( r . left ) , Math . round ( r . top ) , Math . round ( r . width ) , Math . round ( r . height ) ) . catch ( ( ) => { } ) ;
55+ ipc . browserSetBounds ( props . threadId , b . x , b . y , b . w , b . h ) . catch ( ( ) => { } ) ;
4356 } else {
44- ipc . browserOpen ( props . threadId , url , Math . round ( r . left ) , Math . round ( r . top ) , Math . round ( r . width ) , Math . round ( r . height ) )
57+ ipc . browserOpen ( props . threadId , url , b . x , b . y , b . w , b . h )
4558 . then ( ( ) => created . add ( props . threadId ) )
4659 . catch ( ( e ) => console . error ( "browser_open:" , e ) ) ;
4760 }
4861 } , 300 ) ;
4962
5063 const ro = new ResizeObserver ( ( ) => sync ( ) ) ;
51- if ( ref ) ro . observe ( ref ) ;
64+ if ( viewportRef ) ro . observe ( viewportRef ) ;
5265 window . addEventListener ( "resize" , sync ) ;
5366 const poll = setInterval ( sync , 150 ) ;
5467
@@ -61,5 +74,67 @@ export function BrowserPanel(props: Props) {
6174 } ) ;
6275 } ) ;
6376
64- return < div ref = { ref } style = "flex:1;min-height:0;min-width:0;" /> ;
77+ function navigate ( ) {
78+ let u = urlInput ( ) . trim ( ) ;
79+ if ( ! u ) return ;
80+ if ( ! u . match ( / ^ h t t p s ? : \/ \/ / ) ) u = "https://" + u ;
81+ setUrlInput ( u ) ;
82+ setStore ( "threadBrowserUrls" , props . threadId , u ) ;
83+ ipc . browserNavigate ( props . threadId , u ) . catch ( console . error ) ;
84+ }
85+
86+ function close ( ) {
87+ setStore ( "threadBrowserOpen" , props . threadId , false ) ;
88+ ipc . browserHide ( props . threadId ) . catch ( ( ) => { } ) ;
89+ }
90+
91+ return (
92+ < div ref = { viewportRef } class = "bp" >
93+ { /* Address bar — in the DOM, above the native webview */ }
94+ < div ref = { barRef } class = "bp-bar" >
95+ < input
96+ class = "bp-url"
97+ value = { urlInput ( ) }
98+ onInput = { ( e ) => setUrlInput ( e . currentTarget . value ) }
99+ onKeyDown = { ( e ) => { if ( e . key === "Enter" ) { e . currentTarget . blur ( ) ; navigate ( ) ; } } }
100+ onBlur = { navigate }
101+ placeholder = "URL..."
102+ />
103+ < button class = "bp-btn" onClick = { ( ) => ipc . browserDevtools ( props . threadId ) } title = "DevTools" >
104+ < svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" > < polyline points = "16 18 22 12 16 6" /> < polyline points = "8 6 2 12 8 18" /> </ svg >
105+ </ button >
106+ < button class = "bp-btn" onClick = { close } title = "Close" >
107+ < svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" /> < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" /> </ svg >
108+ </ button >
109+ </ div >
110+ { /* The native webview covers the remaining space below the bar */ }
111+ </ div >
112+ ) ;
113+ }
114+
115+ if ( ! document . getElementById ( "bp-styles" ) ) {
116+ const s = document . createElement ( "style" ) ;
117+ s . id = "bp-styles" ;
118+ s . textContent = `
119+ .bp { flex:1; display:flex; flex-direction:column; min-height:0; overflow:hidden; }
120+ .bp-bar {
121+ display:flex; align-items:center; gap:3px; padding:5px 6px;
122+ border-bottom:1px solid var(--border); background:var(--bg-surface); flex-shrink:0;
123+ position:relative; z-index:10;
124+ }
125+ .bp-btn {
126+ width:24px;height:24px;border-radius:var(--radius-sm);
127+ display:flex;align-items:center;justify-content:center;
128+ color:var(--text-tertiary);transition:background .1s,color .1s;flex-shrink:0;
129+ }
130+ .bp-btn:hover { background:var(--bg-accent);color:var(--text-secondary); }
131+ .bp-url {
132+ flex:1;min-width:0;height:24px;padding:0 8px;font-size:12px;
133+ font-family:var(--font-mono);background:var(--bg-muted);
134+ border:1px solid var(--border);border-radius:var(--radius-sm);
135+ color:var(--text);outline:none;
136+ }
137+ .bp-url:focus { border-color:var(--primary); }
138+ ` ;
139+ document . head . appendChild ( s ) ;
65140}
0 commit comments