mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
fix dropdown
This commit is contained in:
parent
e4953057ef
commit
ff9d21eee0
7 changed files with 172 additions and 118 deletions
|
|
@ -32,7 +32,7 @@ function saveStylesFile() {
|
|||
} catch (err) {
|
||||
console.error('[scope-tailwind] Error saving styles.css:', err);
|
||||
}
|
||||
}, 5000);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export const QuickEditChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onC
|
|||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
`
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Re
|
|||
|
||||
return <button
|
||||
type='submit'
|
||||
className={`rounded-full shrink-0 grow-0 cursor-pointer flex items-center justify-center
|
||||
className={`rounded-full flex-shrink-0 flex-grow-0 cursor-pointer flex items-center justify-center
|
||||
${disabled ? 'bg-vscode-disabled-fg' : 'bg-white'}
|
||||
${className}
|
||||
`}
|
||||
|
|
@ -188,7 +188,7 @@ export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Re
|
|||
export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
return <button
|
||||
className={`rounded-full shrink-0 grow-0 cursor-pointer flex items-center justify-center
|
||||
className={`rounded-full flex-shrink-0 flex-grow-0 cursor-pointer flex items-center justify-center
|
||||
bg-white
|
||||
${className}
|
||||
`}
|
||||
|
|
@ -297,7 +297,7 @@ export const SelectedFiles = (
|
|||
select-none
|
||||
bg-void-bg-3 hover:brightness-95
|
||||
text-void-fg-1 text-xs text-nowrap
|
||||
border border-vscode-commandcenter-border rounded-xs
|
||||
border border-void-border-2 rounded-xs
|
||||
`}
|
||||
onClick={() => {
|
||||
// open the file if it is a file
|
||||
|
|
@ -629,7 +629,7 @@ export const SidebarChat = () => {
|
|||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
max-h-[80vh] overflow-y-auto
|
||||
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
|
|
@ -700,7 +700,9 @@ export const SidebarChat = () => {
|
|||
{/* submit options */}
|
||||
<div className='max-w-[150px]
|
||||
@@[&_select]:!void-border-none
|
||||
@@[&_select]:!void-outline-none'
|
||||
@@[&_select]:!void-outline-none
|
||||
flex-grow
|
||||
'
|
||||
>
|
||||
<ModelDropdown featureName='Ctrl+L' />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { CodeEditorWidget } from '../../../../../../../editor/browser/widget/cod
|
|||
import { useAccessor } from './services.js';
|
||||
import { ScrollableElement } from '../../../../../../../base/browser/ui/scrollbar/scrollableElement.js';
|
||||
import { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
|
||||
// type guard
|
||||
|
|
@ -200,114 +201,153 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
|
|||
|
||||
|
||||
export const VoidCustomSelectBox = <T extends any>({
|
||||
options,
|
||||
selectedOption,
|
||||
onChangeOption,
|
||||
getOptionName,
|
||||
getOptionsEqual
|
||||
options,
|
||||
selectedOption,
|
||||
onChangeOption,
|
||||
getOptionName,
|
||||
getOptionsEqual,
|
||||
className,
|
||||
arrowTouchesText = true,
|
||||
}: {
|
||||
options: T[],
|
||||
selectedOption?: T,
|
||||
onChangeOption: (newValue: T) => void,
|
||||
getOptionName: (option: T) => string,
|
||||
getOptionsEqual: (a: T, b: T) => boolean
|
||||
options: T[];
|
||||
selectedOption?: T;
|
||||
onChangeOption: (newValue: T) => void;
|
||||
getOptionName: (option: T) => string;
|
||||
getOptionsEqual: (a: T, b: T) => boolean;
|
||||
className?: string;
|
||||
arrowTouchesText?: boolean;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
if (!selectedOption) {
|
||||
selectedOption = options[0];
|
||||
}
|
||||
if (!selectedOption) {
|
||||
selectedOption = options[0];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
const updatePosition = () => {
|
||||
if (!buttonRef.current) return;
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
const spaceBelow = viewportHeight - rect.bottom;
|
||||
const spaceNeeded = options.length * 28; // Approximate height per option
|
||||
const showAbove = spaceBelow < spaceNeeded && rect.top > spaceBelow;
|
||||
|
||||
// Calculate dropdown position
|
||||
const getDropdownPosition = () => {
|
||||
if (!buttonRef.current) return { top: 0, left: 0 };
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
return {
|
||||
top: rect.bottom + window.scrollY,
|
||||
left: rect.left + window.scrollX,
|
||||
minWidth: rect.width // Ensure dropdown is at least as wide as the button
|
||||
};
|
||||
};
|
||||
setPosition({
|
||||
top: showAbove ? rect.top - 4 - spaceNeeded : rect.bottom + 4,
|
||||
left: rect.left,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative inline-block">
|
||||
{/* Select Button */}
|
||||
<button
|
||||
ref={buttonRef}
|
||||
style={{ fontSize: '6px' }}
|
||||
className="flex items-center gap-1 px-2 h-4 bg-transparent whitespace-nowrap text-void-fg-3"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<span>{getOptionName(selectedOption)}</span>
|
||||
<svg className="w-2 h-2 flex-shrink-0" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M2.5 4.5L6 8L9.5 4.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
updatePosition();
|
||||
window.addEventListener('scroll', updatePosition, true);
|
||||
window.addEventListener('resize', updatePosition);
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="fixed z-10 py-1 bg-void-bg-1 border-void-bg-1 border rounded shadow-lg"
|
||||
style={getDropdownPosition()}
|
||||
>
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionName(option);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updatePosition, true);
|
||||
window.removeEventListener('resize', updatePosition);
|
||||
};
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optionName}
|
||||
className={`flex items-center h-4 px-2 cursor-pointer whitespace-nowrap text-[6px]
|
||||
transition-colors duration-100
|
||||
hover:bg-opacity-95 hover:brightness-105
|
||||
active:bg-opacity-90 active:brightness-110
|
||||
${thisOptionIsSelected ? 'bg-opacity-90 brightness-110' : ''}
|
||||
`}
|
||||
onClick={() => {
|
||||
onChangeOption(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-4 flex justify-center flex-shrink-0">
|
||||
{thisOptionIsSelected && (
|
||||
<svg className="w-2 h-2" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M10 3L4.5 8.5L2 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span>{optionName}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`inline-block ${className}`}
|
||||
>
|
||||
{/* Select Button */}
|
||||
<button
|
||||
ref={buttonRef}
|
||||
className="flex items-center h-4 bg-transparent whitespace-nowrap hover:brightness-110 w-full"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
if (!isOpen) {
|
||||
setTimeout(updatePosition, 0);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className={`max-w-[120px] truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
|
||||
{getOptionName(selectedOption)}
|
||||
</span>
|
||||
<svg
|
||||
className={`size-3 flex-shrink-0 ${arrowTouchesText ? '' : 'ml-auto'}`}
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4.5L6 8L9.5 4.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg w-fit"
|
||||
style={{
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
minWidth: buttonRef.current?.offsetWidth,
|
||||
}}
|
||||
>
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionName(option);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optionName}
|
||||
className={`flex items-center px-2 py-1 cursor-pointer whitespace-nowrap
|
||||
transition-all duration-100
|
||||
bg-void-bg-1
|
||||
${thisOptionIsSelected ? 'bg-void-bg-3' : 'hover:bg-void-bg-2'}
|
||||
`}
|
||||
onClick={() => {
|
||||
onChangeOption(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-4 flex justify-center flex-shrink-0">
|
||||
{thisOptionIsSelected && (
|
||||
<svg className="size-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M10 3L4.5 8.5L2 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span>{optionName}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
|
|||
onChangeOption={onChangeOption}
|
||||
getOptionName={(option) => option.name}
|
||||
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
|
||||
|
||||
className={`text-xs text-void-fg-3 px-1`}
|
||||
/>
|
||||
}
|
||||
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidCheckBox, VoidInputBox, _VoidSelectBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { VoidCheckBox, VoidInputBox, _VoidSelectBox, VoidSwitch, VoidCustomSelectBox } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
|
|
@ -85,28 +85,37 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
|
|||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const providerNameRef = useRef<ProviderName | null>(null)
|
||||
// const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const [providerName, setProviderName] = useState<ProviderName | null>(null)
|
||||
|
||||
const modelNameRef = useRef<string | null>(null)
|
||||
|
||||
const [errorString, setErrorString] = useState('')
|
||||
|
||||
|
||||
const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: displayInfoOfProviderName(providerName).title, value: providerName })), [providerNames])
|
||||
|
||||
return <>
|
||||
<div className='flex items-center gap-4'>
|
||||
|
||||
{/* provider */}
|
||||
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
|
||||
<_VoidSelectBox
|
||||
<VoidCustomSelectBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setProviderName(pn)}
|
||||
getOptionName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
className={`max-w-40 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root
|
||||
py-[4px] px-[6px]
|
||||
`}
|
||||
arrowTouchesText={false}
|
||||
/>
|
||||
{/* <_VoidSelectBox
|
||||
onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state
|
||||
onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])}
|
||||
options={providerOptions}
|
||||
/>
|
||||
</div>
|
||||
/> */}
|
||||
|
||||
{/* model */}
|
||||
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
|
||||
<div className='max-w-40 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'>
|
||||
<VoidInputBox
|
||||
placeholder='Model Name'
|
||||
onChangeText={useCallback((modelName) => { modelNameRef.current = modelName }, [])}
|
||||
|
|
@ -119,7 +128,6 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
|
|||
<button
|
||||
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
|
||||
onClick={() => {
|
||||
const providerName = providerNameRef.current
|
||||
const modelName = modelNameRef.current
|
||||
|
||||
if (providerName === null) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ module.exports = {
|
|||
"void-fg-3": "var(--vscode-input-placeholderForeground)",
|
||||
"void-warning": "var(--vscode-charts-orange)",
|
||||
|
||||
"void-border-1": "var(--vscode-commandCenter-activeBorder)",
|
||||
"void-border-2": "var(--vscode-commandCenter-border)",
|
||||
"void-border-3": "var(--vscode-commandCenter-inactiveBorder)",
|
||||
|
||||
|
||||
vscode: {
|
||||
// see: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
|
||||
|
|
|
|||
Loading…
Reference in a new issue