Skip to content

Commit 8baca40

Browse files
fix(ui): design review polish pass
User bubbles tinted primary-blue instead of invisible gray. Copy button is now an SVG icon (checkmark on copied). Meta tags use dot separators instead of pill backgrounds. Sidebar title gets gradient branding. Top accent line restored as a proper gradient. Browser Y_OFFSET adjusted to 25px.
1 parent 5a77f55 commit 8baca40

5 files changed

Lines changed: 140 additions & 146 deletions

File tree

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,67 @@
1-
import { onMount, onCleanup } from "solid-js";
1+
import { createSignal, onMount, onCleanup } from "solid-js";
22
import { appStore } from "../../stores/app-store";
33
import * as ipc from "../../ipc";
44

5-
// Track created webviews
65
const created = new Set<string>();
76

87
interface 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-
*/
1811
export 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(/^https?:\/\//)) 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
}

crates/tauri-app/frontend/src/components/chat/ChatArea.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,16 @@ function MessageBubble(props: { msg: any; isGenerating: boolean; isLast: boolean
237237
</div>
238238
</Show>
239239
<Show when={isAssistant() && props.msg.content}>
240-
<button class="copy-btn" onClick={copyContent}>
241-
{copied() ? "Copied" : "Copy"}
240+
<button class="copy-btn" onClick={copyContent} title={copied() ? "Copied!" : "Copy"}>
241+
<Show when={copied()} fallback={
242+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
243+
<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
244+
</svg>
245+
}>
246+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--green)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
247+
<polyline points="20 6 9 17 4 12"/>
248+
</svg>
249+
</Show>
242250
</button>
243251
</Show>
244252
</div>
@@ -390,8 +398,9 @@ if (!document.getElementById("chat-styles")) {
390398
word-break: break-word;
391399
}
392400
.message-user .message-bubble {
393-
background: var(--bg-user-bubble);
394-
border: 1px solid var(--border);
401+
background: rgba(107, 124, 255, 0.08);
402+
border: 1px solid rgba(107, 124, 255, 0.12);
403+
color: var(--text);
395404
}
396405
.message-assistant .message-bubble {
397406
color: var(--text);
@@ -422,20 +431,24 @@ if (!document.getElementById("chat-styles")) {
422431
font-size: 10px;
423432
font-weight: 500;
424433
color: var(--text-tertiary);
425-
background: var(--bg-muted);
426-
padding: 1px 6px;
427-
border-radius: 3px;
434+
padding: 1px 0;
428435
font-family: var(--font-mono);
429436
white-space: nowrap;
430437
}
438+
.meta-tag + .meta-tag::before {
439+
content: "·";
440+
margin-right: 6px;
441+
color: var(--text-tertiary);
442+
opacity: 0.4;
443+
}
431444
.copy-btn {
432-
font-size: 11px;
433-
font-weight: 500;
434445
color: var(--text-tertiary);
435-
padding: 2px 8px;
446+
padding: 3px;
436447
border-radius: var(--radius-sm);
437448
transition: all 0.12s;
438449
margin-left: auto;
450+
display: flex;
451+
align-items: center;
439452
}
440453
.copy-btn:hover { background: var(--bg-accent); color: var(--text-secondary); }
441454

crates/tauri-app/frontend/src/components/sidebar/Sidebar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,11 @@ export function Sidebar() {
155155
.sidebar-title {
156156
font-size: 14px;
157157
font-weight: 700;
158-
color: var(--text);
159158
letter-spacing: -0.3px;
159+
background: linear-gradient(135deg, var(--text) 40%, var(--primary));
160+
-webkit-background-clip: text;
161+
-webkit-text-fill-color: transparent;
162+
background-clip: text;
160163
}
161164
.icon-btn {
162165
color: var(--text-tertiary);

crates/tauri-app/frontend/src/components/tabs/TabBar.tsx

Lines changed: 2 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DragDropProvider } from "@dnd-kit/solid";
33
import { useSortable } from "@dnd-kit/solid/sortable";
44
import { move } from "@dnd-kit/helpers";
55
import { appStore } from "../../stores/app-store";
6-
import * as ipc from "../../ipc";
6+
import { browserHide } from "../../ipc";
77

88
function SortableTab(props: { tabId: string; index: number }) {
99
const { store, setStore, closeTab } = appStore;
@@ -51,8 +51,6 @@ export function TabBar() {
5151
const { store, setStore } = appStore;
5252
const [showCopied, setShowCopied] = createSignal(false);
5353

54-
const [urlInput, setUrlInput] = createSignal("");
55-
5654
const browserOpen = () => {
5755
const tab = store.activeTab;
5856
return tab ? !!store.threadBrowserOpen[tab] : false;
@@ -63,53 +61,7 @@ export function TabBar() {
6361
if (!tab) return;
6462
const opening = !store.threadBrowserOpen[tab];
6563
setStore("threadBrowserOpen", tab, opening);
66-
if (opening) setUrlInput(store.threadBrowserUrls[tab] || "https://google.com");
67-
if (!opening) ipc.browserHide(tab).catch(() => {});
68-
}
69-
70-
function navigateBrowser() {
71-
const tab = store.activeTab;
72-
if (!tab) return;
73-
let u = urlInput().trim();
74-
if (!u) return;
75-
if (!u.match(/^https?:\/\//)) u = "https://" + u;
76-
setUrlInput(u);
77-
setStore("threadBrowserUrls", tab, u);
78-
ipc.browserNavigate(tab, u).catch(console.error);
79-
}
80-
81-
function closeBrowser() {
82-
const tab = store.activeTab;
83-
if (!tab) return;
84-
setStore("threadBrowserOpen", tab, false);
85-
ipc.browserHide(tab).catch(() => {});
86-
}
87-
88-
function openDevtools() {
89-
const tab = store.activeTab;
90-
if (tab) ipc.browserDevtools(tab).catch(console.error);
91-
}
92-
93-
function inspectElement() {
94-
const tab = store.activeTab;
95-
if (!tab) return;
96-
ipc.browserEval(tab, `
97-
(function(){
98-
if(window.__cfI)return;window.__cfI=true;
99-
var o=document.createElement('div');
100-
o.style.cssText='position:fixed;pointer-events:none;z-index:999999;border:2px solid #6b7cff;background:rgba(107,124,255,0.08);display:none;transition:all .06s;';
101-
document.body.appendChild(o);var last=null;
102-
function mm(e){var el=document.elementFromPoint(e.clientX,e.clientY);if(!el||el===o)return;last=el;var r=el.getBoundingClientRect();o.style.display='block';o.style.left=r.left+'px';o.style.top=r.top+'px';o.style.width=r.width+'px';o.style.height=r.height+'px';}
103-
function mc(e){e.preventDefault();e.stopPropagation();if(!last)return;
104-
var html=last.outerHTML;if(html.length>4000)html=html.substring(0,4000)+'...';
105-
var cs=getComputedStyle(last),s={},keep=['color','background','background-color','font-size','font-weight','font-family','padding','margin','border','border-radius','display','width','height','position','box-shadow','text-align','line-height','gap','flex-direction','align-items','justify-content'];
106-
var d=document.createElement(last.tagName);document.body.appendChild(d);var ds=getComputedStyle(d);
107-
for(var i=0;i<keep.length;i++){var v=cs.getPropertyValue(keep[i]);var dv=ds.getPropertyValue(keep[i]);if(v&&v!==dv&&v!=='none'&&v!=='normal'&&v!=='auto'&&v!=='0px')s[keep[i]]=v;}d.remove();
108-
document.title='__CF_EXTRACT__'+JSON.stringify({html:html,css:JSON.stringify(s,null,2)});
109-
document.removeEventListener('mousemove',mm,true);document.removeEventListener('click',mc,true);o.remove();window.__cfI=false;}
110-
document.addEventListener('mousemove',mm,true);document.addEventListener('click',mc,true);
111-
})();
112-
`).catch(console.error);
64+
if (!opening) browserHide(tab).catch(() => {});
11365
}
11466

11567
function toggleDiff() {
@@ -179,31 +131,6 @@ export function TabBar() {
179131
</button>
180132
</div>
181133
</Show>
182-
<Show when={browserOpen()}>
183-
<div class="tb-browser-bar">
184-
<input
185-
class="tb-burl"
186-
value={urlInput()}
187-
onInput={(e) => setUrlInput(e.currentTarget.value)}
188-
onKeyDown={(e) => { if (e.key === "Enter") navigateBrowser(); }}
189-
placeholder="URL..."
190-
/>
191-
<button class="tb-bgo" onClick={navigateBrowser}>Go</button>
192-
<button class="tb-action" onClick={inspectElement} title="Select element">
193-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
194-
<path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4z"/>
195-
</svg>
196-
</button>
197-
<button class="tb-action" onClick={openDevtools} title="DevTools">
198-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
199-
<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
200-
</svg>
201-
</button>
202-
<button class="tb-action" onClick={closeBrowser} title="Close browser">
203-
<svg width="11" height="11" 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>
204-
</button>
205-
</div>
206-
</Show>
207134
</div>
208135
</DragDropProvider>
209136
</Show>
@@ -309,41 +236,6 @@ if (!document.getElementById("tab-bar-styles")) {
309236
white-space: nowrap;
310237
pointer-events: none;
311238
}
312-
.tb-browser-bar {
313-
display: flex;
314-
align-items: center;
315-
gap: 3px;
316-
padding: 0 4px 4px;
317-
margin-left: auto;
318-
min-width: 140px;
319-
max-width: 320px;
320-
flex-shrink: 1;
321-
}
322-
.tb-burl {
323-
flex: 1;
324-
min-width: 0;
325-
height: 22px;
326-
padding: 0 6px;
327-
font-size: 11px;
328-
font-family: var(--font-mono);
329-
background: var(--bg-accent);
330-
border: 1px solid var(--border);
331-
border-radius: 4px;
332-
color: var(--text);
333-
outline: none;
334-
}
335-
.tb-burl:focus { border-color: var(--primary); }
336-
.tb-bgo {
337-
height: 22px;
338-
padding: 0 8px;
339-
font-size: 10px;
340-
font-weight: 600;
341-
background: var(--primary);
342-
color: #fff;
343-
border-radius: 4px;
344-
flex-shrink: 0;
345-
}
346-
.tb-bgo:hover { filter: brightness(1.15); }
347239
.tab {
348240
display: flex;
349241
align-items: center;

0 commit comments

Comments
 (0)