Skip to content

Commit 5e9c6ec

Browse files
author
Tim Sinaeve
committed
Remove rounded focus box from database dropdown
1 parent a89ac7c commit 5e9c6ec

2 files changed

Lines changed: 301 additions & 28 deletions

File tree

components/ContextMenu.tsx.bak

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
2+
import ReactDOM from 'react-dom';
3+
4+
export type MenuItem =
5+
| {
6+
label: string;
7+
action: () => void;
8+
icon?: React.FC<{ className?: string }>;
9+
disabled?: boolean;
10+
shortcut?: string;
11+
submenu?: never;
12+
}
13+
| {
14+
label: string;
15+
submenu: MenuItem[];
16+
icon?: React.FC<{ className?: string }>;
17+
disabled?: boolean;
18+
shortcut?: string;
19+
action?: never;
20+
}
21+
| { type: 'separator' };
22+
23+
interface ContextMenuProps {
24+
isOpen: boolean;
25+
position: { x: number; y: number };
26+
items: MenuItem[];
27+
onClose: () => void;
28+
}
29+
30+
const EDGE_MARGIN = 8;
31+
32+
const ContextMenu: React.FC<ContextMenuProps> = ({ isOpen, position, items, onClose }) => {
33+
const menuRef = useRef<HTMLDivElement>(null);
34+
const [openSubmenu, setOpenSubmenu] = useState<string | null>(null);
35+
const [menuStyle, setMenuStyle] = useState<{ top: number; left: number; maxHeight: number; overflowY: React.CSSProperties['overflowY'] }>({
36+
top: position.y,
37+
left: position.x,
38+
maxHeight: 0,
39+
overflowY: 'visible',
40+
});
41+
42+
const recalculatePosition = useCallback(() => {
43+
const menu = menuRef.current;
44+
if (!menu) return;
45+
46+
const { innerWidth, innerHeight } = window;
47+
const rect = menu.getBoundingClientRect();
48+
const maxHeight = Math.max(innerHeight - EDGE_MARGIN * 2, 0);
49+
50+
let left = rect.left;
51+
let top = rect.top;
52+
53+
if (rect.right > innerWidth - EDGE_MARGIN) {
54+
left = Math.max(EDGE_MARGIN, innerWidth - rect.width - EDGE_MARGIN);
55+
}
56+
if (left < EDGE_MARGIN) {
57+
left = EDGE_MARGIN;
58+
}
59+
60+
if (rect.bottom > innerHeight - EDGE_MARGIN) {
61+
top = Math.max(EDGE_MARGIN, innerHeight - rect.height - EDGE_MARGIN);
62+
}
63+
if (top < EDGE_MARGIN) {
64+
top = EDGE_MARGIN;
65+
}
66+
67+
const overflowY: React.CSSProperties['overflowY'] = rect.height > maxHeight ? 'auto' : 'visible';
68+
69+
setMenuStyle((previous) => {
70+
if (
71+
previous.top === top &&
72+
previous.left === left &&
73+
previous.maxHeight === maxHeight &&
74+
previous.overflowY === overflowY
75+
) {
76+
return previous;
77+
}
78+
79+
return {
80+
top,
81+
left,
82+
maxHeight,
83+
overflowY,
84+
};
85+
});
86+
}, []);
87+
88+
useEffect(() => {
89+
if (!isOpen) return;
90+
91+
setOpenSubmenu(null);
92+
93+
const handleClickOutside = (event: MouseEvent) => {
94+
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
95+
onClose();
96+
}
97+
};
98+
const handleKeyDown = (event: KeyboardEvent) => {
99+
if (event.key === 'Escape') {
100+
onClose();
101+
}
102+
}
103+
104+
document.addEventListener('mousedown', handleClickOutside);
105+
window.addEventListener('keydown', handleKeyDown);
106+
return () => {
107+
document.removeEventListener('mousedown', handleClickOutside);
108+
window.removeEventListener('keydown', handleKeyDown);
109+
};
110+
}, [isOpen, onClose]);
111+
112+
useLayoutEffect(() => {
113+
if (!isOpen) return;
114+
115+
setMenuStyle((previous) => {
116+
if (previous.top === position.y && previous.left === position.x) {
117+
return previous;
118+
}
119+
120+
return {
121+
top: position.y,
122+
left: position.x,
123+
maxHeight: previous.maxHeight,
124+
overflowY: previous.overflowY,
125+
};
126+
});
127+
128+
const frame = requestAnimationFrame(() => {
129+
recalculatePosition();
130+
});
131+
132+
return () => cancelAnimationFrame(frame);
133+
}, [isOpen, position.x, position.y, recalculatePosition]);
134+
135+
useLayoutEffect(() => {
136+
if (!isOpen) return;
137+
138+
const frame = requestAnimationFrame(() => {
139+
recalculatePosition();
140+
});
141+
142+
return () => cancelAnimationFrame(frame);
143+
}, [isOpen, items, recalculatePosition]);
144+
145+
useEffect(() => {
146+
if (!isOpen) return;
147+
148+
const handleResize = () => {
149+
recalculatePosition();
150+
};
151+
152+
window.addEventListener('resize', handleResize);
153+
return () => {
154+
window.removeEventListener('resize', handleResize);
155+
};
156+
}, [isOpen, recalculatePosition]);
157+
158+
if (!isOpen) return null;
159+
160+
const overlayRoot = document.getElementById('overlay-root');
161+
if (!overlayRoot) return null;
162+
163+
const hasEnabledItem = (menuItems: MenuItem[]): boolean => {
164+
return menuItems.some(item => {
165+
if ('type' in item) {
166+
return false;
167+
}
168+
169+
if ('submenu' in item) {
170+
return !item.disabled && hasEnabledItem(item.submenu);
171+
}
172+
173+
return !item.disabled;
174+
});
175+
};
176+
177+
const renderItems = (menuItems: MenuItem[], depth = 0) => {
178+
return menuItems.map((item, index) => {
179+
if ('type' in item) {
180+
return <li key={`separator-${depth}-${index}`} className="h-px bg-border-color my-1.5" />;
181+
}
182+
183+
if ('submenu' in item) {
184+
const menuKey = `${depth}-${index}`;
185+
const isOpen = openSubmenu === menuKey;
186+
const hasEnabledSubitem = hasEnabledItem(item.submenu);
187+
const Icon = item.icon;
188+
189+
return (
190+
<li
191+
key={`${item.label}-${depth}-${index}`}
192+
className="relative"
193+
onMouseEnter={() => setOpenSubmenu(menuKey)}
194+
onMouseLeave={() => setOpenSubmenu(null)}
195+
>
196+
<button
197+
onClick={() => {
198+
if (!item.disabled && hasEnabledSubitem) {
199+
setOpenSubmenu(menuKey);
200+
}
201+
}}
202+
disabled={item.disabled || !hasEnabledSubitem}
203+
className="w-full flex items-center justify-between text-left px-2 py-1.5 text-xs rounded-md transition-colors text-text-main disabled:text-text-secondary/50 disabled:cursor-not-allowed hover:bg-primary hover:text-primary-text focus:bg-primary focus:text-primary-text focus:outline-none"
204+
>
205+
<div className="flex items-center gap-3">
206+
{Icon && <Icon className="w-4 h-4" />}
207+
<span>{item.label}</span>
208+
</div>
209+
<span className="text-text-secondary">›</span>
210+
</button>
211+
{isOpen && item.submenu.length > 0 && (
212+
<div className="absolute top-0 left-full ml-1 z-10 w-[16.8rem] rounded-md bg-secondary p-1.5 shadow-2xl border border-border-color animate-fade-in-fast">
213+
<ul className="space-y-1">{renderItems(item.submenu, depth + 1)}</ul>
214+
</div>
215+
)}
216+
</li>
217+
);
218+
}
219+
220+
const { label, action, icon: Icon, disabled, shortcut } = item;
221+
222+
return (
223+
<li key={`${label}-${depth}-${index}`}>
224+
<button
225+
onClick={() => {
226+
if (!disabled) {
227+
action();
228+
onClose();
229+
}
230+
}}
231+
disabled={disabled}
232+
className="w-full flex items-center justify-between text-left px-2 py-1.5 text-xs rounded-md transition-colors text-text-main disabled:text-text-secondary/50 disabled:cursor-not-allowed hover:bg-primary hover:text-primary-text focus:bg-primary focus:text-primary-text focus:outline-none"
233+
>
234+
<div className="flex items-center gap-3">
235+
{Icon && <Icon className="w-4 h-4" />}
236+
<span>{label}</span>
237+
</div>
238+
{shortcut && <span className="text-xs text-text-secondary">{shortcut}</span>}
239+
</button>
240+
</li>
241+
);
242+
});
243+
};
244+
245+
return ReactDOM.createPortal(
246+
<div
247+
ref={menuRef}
248+
style={{
249+
top: menuStyle.top,
250+
left: menuStyle.left,
251+
maxHeight: menuStyle.maxHeight ? menuStyle.maxHeight : undefined,
252+
overflowY: menuStyle.overflowY,
253+
}}
254+
className="fixed z-50 w-[16.8rem] rounded-md bg-secondary p-1.5 shadow-2xl border border-border-color animate-fade-in-fast"
255+
>
256+
<ul className="space-y-1">
257+
{renderItems(items)}
258+
</ul>
259+
<style>{`
260+
@keyframes fade-in-fast {
261+
from { opacity: 0; transform: scale(0.95); }
262+
to { opacity: 1; transform: scale(1); }
263+
}
264+
.animate-fade-in-fast {
265+
animation: fade-in-fast 0.1s ease-out forwards;
266+
}
267+
`}</style>
268+
</div>,
269+
overlayRoot
270+
);
271+
};
272+
273+
export default ContextMenu;

components/StatusBar.tsx

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -117,31 +117,31 @@ const ZoomButton: React.FC<ZoomButtonProps> = ({ hint, icon, className = '', dis
117117
};
118118

119119
const StatusBar: React.FC<StatusBarProps> = ({
120-
status,
121-
modelName,
122-
llmProviderName,
123-
llmProviderUrl,
124-
documentCount,
125-
lastSaved,
126-
availableModels,
127-
onModelChange,
128-
discoveredServices,
129-
onProviderChange,
130-
appVersion,
131-
databasePath,
132-
databaseStatus,
133-
onDatabaseMenu,
134-
onOpenAbout,
135-
previewScale,
136-
onPreviewZoomIn,
137-
onPreviewZoomOut,
138-
onPreviewReset,
139-
isPreviewZoomAvailable,
140-
previewMinScale,
141-
previewMaxScale,
142-
previewInitialScale,
143-
previewMetadata,
144-
zoomTarget,
120+
status,
121+
modelName,
122+
llmProviderName,
123+
llmProviderUrl,
124+
documentCount,
125+
lastSaved,
126+
availableModels,
127+
onModelChange,
128+
discoveredServices,
129+
onProviderChange,
130+
appVersion,
131+
databasePath,
132+
databaseStatus,
133+
onDatabaseMenu,
134+
onOpenAbout,
135+
previewScale,
136+
onPreviewZoomIn,
137+
onPreviewZoomOut,
138+
onPreviewReset,
139+
isPreviewZoomAvailable,
140+
previewMinScale,
141+
previewMaxScale,
142+
previewInitialScale,
143+
previewMetadata,
144+
zoomTarget,
145145
}) => {
146146
const { text, color, tooltip } = statusConfig[status];
147147
const selectedService = discoveredServices.find(s => s.generateUrl === llmProviderUrl);
@@ -292,8 +292,8 @@ const StatusBar: React.FC<StatusBarProps> = ({
292292
const sizeText = `${previewMetadata.width} × ${previewMetadata.height} px`;
293293
const typeText = previewMetadata.mimeType
294294
? (previewMetadata.mimeType.startsWith('image/')
295-
? previewMetadata.mimeType.replace('image/', '').toUpperCase()
296-
: previewMetadata.mimeType.toUpperCase())
295+
? previewMetadata.mimeType.replace('image/', '').toUpperCase()
296+
: previewMetadata.mimeType.toUpperCase())
297297
: null;
298298
return {
299299
label: baseLabel,
@@ -373,7 +373,7 @@ const StatusBar: React.FC<StatusBarProps> = ({
373373
event.preventDefault();
374374
handleDatabaseMenu(event);
375375
}}
376-
className={`flex items-center gap-1.5 px-1.5 py-1 -my-1 rounded-md transition-colors ${onDatabaseMenu ? 'hover:bg-border-color focus:outline-none focus:ring-1 focus:ring-primary cursor-pointer' : 'cursor-default'}`}
376+
className={`flex items-center gap-1.5 px-1.5 py-1 -my-1 transition-colors ${onDatabaseMenu ? 'hover:text-text-main focus:outline-none focus:text-text-main cursor-pointer' : 'cursor-default'}`}
377377
disabled={!onDatabaseMenu}
378378
ref={databaseTriggerRef}
379379
onMouseEnter={() => setShowDatabaseTooltip(true)}

0 commit comments

Comments
 (0)