mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-02-19 03:14:29 -05:00
feat(ui): make tool list panel resizable (#2253)
## Description Add draggable resize handle to tool list panel with min/max width constraints, visual feedback, and localStorage persistence. ## PR Checklist > Thank you for opening a Pull Request! Before submitting your PR, there are a > few things you can do to make sure it goes smoothly: - [ ] Make sure you reviewed [CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md) - [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) - [ ] Make sure to add `!` if this involve a breaking change 🛠️ Fixes #1729 --------- Co-authored-by: Wenxin Du <117315983+duwenxin99@users.noreply.github.com>
This commit is contained in:
@@ -87,8 +87,29 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
max-width: 50vw;
|
||||
overflow: visible;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
cursor: ew-resize;
|
||||
background-color: transparent;
|
||||
z-index: 10;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.resize-handle:hover,
|
||||
.resize-handle.active {
|
||||
background-color: var(--toolbox-blue);
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
@@ -626,10 +647,13 @@ body {
|
||||
.search-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-bottom: 15px;
|
||||
box-sizing: border-box;
|
||||
|
||||
#toolset-search-input {
|
||||
flex-grow: 1;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 20px 0 0 20px;
|
||||
@@ -637,6 +661,7 @@ body {
|
||||
font-family: inherit;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-primary-gray);
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
||||
116
internal/server/static/js/resize.js
Normal file
116
internal/server/static/js/resize.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const STORAGE_KEY = 'toolbox-second-nav-width';
|
||||
const DEFAULT_WIDTH = 250;
|
||||
const MIN_WIDTH = 200;
|
||||
const MAX_WIDTH_PERCENT = 50;
|
||||
|
||||
/**
|
||||
* Creates and attaches a resize handle to the second navigation panel
|
||||
*/
|
||||
export function initializeResize() {
|
||||
const secondNav = document.querySelector('.second-nav');
|
||||
if (!secondNav) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create resize handle
|
||||
const resizeHandle = document.createElement('div');
|
||||
resizeHandle.className = 'resize-handle';
|
||||
resizeHandle.setAttribute('aria-label', 'Resize panel');
|
||||
secondNav.appendChild(resizeHandle);
|
||||
|
||||
// Load saved width or use default
|
||||
let initialWidth = DEFAULT_WIDTH;
|
||||
try {
|
||||
const savedWidth = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedWidth) {
|
||||
const parsed = parseInt(savedWidth, 10);
|
||||
if (!isNaN(parsed) && parsed >= MIN_WIDTH) {
|
||||
initialWidth = parsed;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// localStorage may be unavailable in private browsing mode
|
||||
console.warn('Failed to load saved panel width:', e);
|
||||
}
|
||||
setPanelWidth(secondNav, initialWidth);
|
||||
|
||||
// Setup resize functionality
|
||||
let startX = 0;
|
||||
let startWidth = 0;
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
const deltaX = e.clientX - startX;
|
||||
const newWidth = startWidth + deltaX;
|
||||
const maxWidth = (window.innerWidth * MAX_WIDTH_PERCENT) / 100;
|
||||
|
||||
const clampedWidth = Math.max(MIN_WIDTH, Math.min(newWidth, maxWidth));
|
||||
setPanelWidth(secondNav, clampedWidth);
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
|
||||
resizeHandle.classList.remove('active');
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
|
||||
// Save width to localStorage
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, secondNav.offsetWidth.toString());
|
||||
} catch (e) {
|
||||
// localStorage may be unavailable in private browsing mode
|
||||
console.warn('Failed to save panel width:', e);
|
||||
}
|
||||
};
|
||||
|
||||
resizeHandle.addEventListener('mousedown', (e) => {
|
||||
startX = e.clientX;
|
||||
startWidth = secondNav.offsetWidth;
|
||||
resizeHandle.classList.add('active');
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
e.preventDefault();
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
});
|
||||
|
||||
// Handle window resize to enforce max width
|
||||
window.addEventListener('resize', () => {
|
||||
const currentWidth = secondNav.offsetWidth;
|
||||
const maxWidth = (window.innerWidth * MAX_WIDTH_PERCENT) / 100;
|
||||
|
||||
if (currentWidth > maxWidth) {
|
||||
setPanelWidth(secondNav, maxWidth);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, maxWidth.toString());
|
||||
} catch (e) {
|
||||
// localStorage may be unavailable in private browsing mode
|
||||
console.warn('Failed to save panel width:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the width of the panel and updates flex property
|
||||
*/
|
||||
function setPanelWidth(panel, width) {
|
||||
panel.style.flex = `0 0 ${width}px`;
|
||||
}
|
||||
|
||||
@@ -22,12 +22,16 @@
|
||||
<script type="module" src="/ui/js/tools.js"></script>
|
||||
<script src="/ui/js/navbar.js"></script>
|
||||
<script src="/ui/js/mainContent.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
<script type="module">
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
const activeNav = navbarContainer.getAttribute('data-active-nav');
|
||||
renderNavbar('navbar-container', activeNav);
|
||||
renderMainContent('main-content-container', 'tool-display-area', getToolInstructions())
|
||||
renderMainContent('main-content-container', 'tool-display-area', getToolInstructions());
|
||||
|
||||
// Initialize resize functionality
|
||||
const { initializeResize } = await import('/ui/js/resize.js');
|
||||
initializeResize();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -29,12 +29,16 @@
|
||||
<script type="module" src="/ui/js/toolsets.js"></script>
|
||||
<script src="/ui/js/navbar.js"></script>
|
||||
<script src="/ui/js/mainContent.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
<script type="module">
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
const activeNav = navbarContainer.getAttribute('data-active-nav');
|
||||
renderNavbar('navbar-container', activeNav);
|
||||
renderMainContent('main-content-container', 'tool-display-area', getToolsetInstructions());
|
||||
|
||||
// Initialize resize functionality
|
||||
const { initializeResize } = await import('/ui/js/resize.js');
|
||||
initializeResize();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user