mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merge pull request #1329 from RooVetGit/chat_custom_dropdown
Custom dropdowns for mode/api profile
This commit is contained in:
commit
2a4756aecd
5 changed files with 473 additions and 125 deletions
|
|
@ -15,8 +15,8 @@ import Thumbnails from "../common/Thumbnails"
|
|||
import { vscode } from "../../utils/vscode"
|
||||
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
|
||||
import { Mode, getAllModes } from "../../../../src/shared/modes"
|
||||
import { CaretIcon } from "../common/CaretIcon"
|
||||
import { convertToMentionPath } from "../../utils/path-mentions"
|
||||
import { SelectDropdown, DropdownOptionType } from "../ui"
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
inputValue: string
|
||||
|
|
@ -541,35 +541,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
[updateCursorPosition],
|
||||
)
|
||||
|
||||
const selectStyle = {
|
||||
fontSize: "11px",
|
||||
cursor: textAreaDisabled ? "not-allowed" : "pointer",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
color: "var(--vscode-foreground)",
|
||||
opacity: textAreaDisabled ? 0.5 : 0.8,
|
||||
outline: "none",
|
||||
paddingLeft: "20px",
|
||||
paddingRight: "6px",
|
||||
WebkitAppearance: "none" as const,
|
||||
MozAppearance: "none" as const,
|
||||
appearance: "none" as const,
|
||||
}
|
||||
|
||||
const optionStyle = {
|
||||
backgroundColor: "var(--vscode-dropdown-background)",
|
||||
color: "var(--vscode-dropdown-foreground)",
|
||||
}
|
||||
|
||||
const caretContainerStyle = {
|
||||
position: "absolute" as const,
|
||||
left: 6,
|
||||
top: "50%",
|
||||
transform: "translateY(-45%)",
|
||||
pointerEvents: "none" as const,
|
||||
opacity: textAreaDisabled ? 0.5 : 0.8,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="chat-text-area"
|
||||
|
|
@ -791,122 +762,110 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
marginTop: "auto",
|
||||
paddingTop: "2px",
|
||||
}}>
|
||||
{/* Left side - dropdowns container */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
overflow: "hidden",
|
||||
minWidth: 0,
|
||||
}}>
|
||||
<div style={{ position: "relative", display: "inline-block" }}>
|
||||
<select
|
||||
{/* Mode selector - fixed width */}
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<SelectDropdown
|
||||
value={mode}
|
||||
disabled={textAreaDisabled}
|
||||
title="Select mode for interaction"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === "prompts-action") {
|
||||
window.postMessage({ type: "action", action: "promptsButtonClicked" })
|
||||
return
|
||||
}
|
||||
options={[
|
||||
// Add the shortcut text as a disabled option at the top
|
||||
{
|
||||
value: "shortcut",
|
||||
label: modeShortcutText,
|
||||
disabled: true,
|
||||
type: DropdownOptionType.SHORTCUT,
|
||||
},
|
||||
// Add all modes
|
||||
...getAllModes(customModes).map((mode) => ({
|
||||
value: mode.slug,
|
||||
label: mode.name,
|
||||
type: DropdownOptionType.ITEM,
|
||||
})),
|
||||
// Add separator
|
||||
{
|
||||
value: "sep-1",
|
||||
label: "Separator",
|
||||
type: DropdownOptionType.SEPARATOR,
|
||||
},
|
||||
// Add Edit option
|
||||
{
|
||||
value: "promptsButtonClicked",
|
||||
label: "Edit...",
|
||||
type: DropdownOptionType.ACTION,
|
||||
},
|
||||
]}
|
||||
onChange={(value) => {
|
||||
setMode(value as Mode)
|
||||
vscode.postMessage({
|
||||
type: "mode",
|
||||
text: value,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
...selectStyle,
|
||||
minWidth: "70px",
|
||||
flex: "0 0 auto",
|
||||
}}>
|
||||
<option
|
||||
disabled
|
||||
style={{ ...optionStyle, fontStyle: "italic", opacity: 0.6, padding: "2px 8px" }}>
|
||||
{modeShortcutText}
|
||||
</option>
|
||||
{getAllModes(customModes).map((mode) => (
|
||||
<option key={mode.slug} value={mode.slug} style={{ ...optionStyle }}>
|
||||
{mode.name}
|
||||
</option>
|
||||
))}
|
||||
<option
|
||||
disabled
|
||||
style={{
|
||||
borderTop: "1px solid var(--vscode-dropdown-border)",
|
||||
...optionStyle,
|
||||
}}>
|
||||
────
|
||||
</option>
|
||||
<option value="prompts-action" style={{ ...optionStyle }}>
|
||||
Edit...
|
||||
</option>
|
||||
</select>
|
||||
<div style={caretContainerStyle}>
|
||||
<CaretIcon />
|
||||
</div>
|
||||
shortcutText={modeShortcutText}
|
||||
triggerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* API configuration selector - flexible width */}
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
display: "inline-block",
|
||||
flex: "1 1 auto",
|
||||
minWidth: 0,
|
||||
maxWidth: "150px",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<select
|
||||
<SelectDropdown
|
||||
value={currentApiConfigName || ""}
|
||||
disabled={textAreaDisabled}
|
||||
title="Select API configuration"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === "settings-action") {
|
||||
window.postMessage({ type: "action", action: "settingsButtonClicked" })
|
||||
return
|
||||
}
|
||||
options={[
|
||||
// Add all API configurations
|
||||
...(listApiConfigMeta || []).map((config) => ({
|
||||
value: config.name,
|
||||
label: config.name,
|
||||
type: DropdownOptionType.ITEM,
|
||||
})),
|
||||
// Add separator
|
||||
{
|
||||
value: "sep-2",
|
||||
label: "Separator",
|
||||
type: DropdownOptionType.SEPARATOR,
|
||||
},
|
||||
// Add Edit option
|
||||
{
|
||||
value: "settingsButtonClicked",
|
||||
label: "Edit...",
|
||||
type: DropdownOptionType.ACTION,
|
||||
},
|
||||
]}
|
||||
onChange={(value) => {
|
||||
vscode.postMessage({
|
||||
type: "loadApiConfiguration",
|
||||
text: value,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
...selectStyle,
|
||||
width: "100%",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
{(listApiConfigMeta || []).map((config) => (
|
||||
<option
|
||||
key={config.name}
|
||||
value={config.name}
|
||||
style={{
|
||||
...optionStyle,
|
||||
}}>
|
||||
{config.name}
|
||||
</option>
|
||||
))}
|
||||
<option
|
||||
disabled
|
||||
style={{
|
||||
borderTop: "1px solid var(--vscode-dropdown-border)",
|
||||
...optionStyle,
|
||||
}}>
|
||||
────
|
||||
</option>
|
||||
<option value="settings-action" style={{ ...optionStyle }}>
|
||||
Edit...
|
||||
</option>
|
||||
</select>
|
||||
<div style={caretContainerStyle}>
|
||||
<CaretIcon />
|
||||
</div>
|
||||
contentClassName="max-h-[300px] overflow-y-auto"
|
||||
triggerClassName="w-full text-ellipsis overflow-hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - action buttons */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
gap: "8px",
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
{isEnhancingPrompt ? (
|
||||
|
|
@ -916,7 +875,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
color: "var(--vscode-input-foreground)",
|
||||
opacity: 0.5,
|
||||
fontSize: 16.5,
|
||||
marginRight: 10,
|
||||
marginRight: 6,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import React from "react"
|
||||
|
||||
export const CaretIcon = () => (
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
)
|
||||
246
webview-ui/src/components/ui/__tests__/select-dropdown.test.tsx
Normal file
246
webview-ui/src/components/ui/__tests__/select-dropdown.test.tsx
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
import React, { ReactNode } from "react"
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { SelectDropdown, DropdownOptionType } from "../select-dropdown"
|
||||
|
||||
// Mock window.postMessage
|
||||
const postMessageMock = jest.fn()
|
||||
Object.defineProperty(window, "postMessage", {
|
||||
writable: true,
|
||||
value: postMessageMock,
|
||||
})
|
||||
|
||||
// Mock the Radix UI DropdownMenu component and its children
|
||||
jest.mock("../dropdown-menu", () => {
|
||||
return {
|
||||
DropdownMenu: ({ children }: { children: ReactNode }) => <div data-testid="dropdown-root">{children}</div>,
|
||||
|
||||
DropdownMenuTrigger: ({
|
||||
children,
|
||||
disabled,
|
||||
...props
|
||||
}: {
|
||||
children: ReactNode
|
||||
disabled?: boolean
|
||||
[key: string]: any
|
||||
}) => (
|
||||
<button data-testid="dropdown-trigger" disabled={disabled} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
|
||||
DropdownMenuContent: ({ children }: { children: ReactNode }) => (
|
||||
<div data-testid="dropdown-content">{children}</div>
|
||||
),
|
||||
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
}: {
|
||||
children: ReactNode
|
||||
onClick?: () => void
|
||||
disabled?: boolean
|
||||
}) => (
|
||||
<div data-testid="dropdown-item" onClick={onClick} aria-disabled={disabled}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
|
||||
DropdownMenuSeparator: () => <div data-testid="dropdown-separator" />,
|
||||
}
|
||||
})
|
||||
|
||||
describe("SelectDropdown", () => {
|
||||
const options = [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
{ value: "sep-1", label: "────", disabled: true },
|
||||
{ value: "action", label: "Action Item" },
|
||||
]
|
||||
|
||||
const onChangeMock = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders correctly with default props", () => {
|
||||
render(<SelectDropdown value="option1" options={options} onChange={onChangeMock} />)
|
||||
|
||||
// Check that the selected option is displayed in the trigger, not in a menu item
|
||||
const trigger = screen.getByTestId("dropdown-trigger")
|
||||
expect(trigger).toHaveTextContent("Option 1")
|
||||
})
|
||||
|
||||
it("handles disabled state correctly", () => {
|
||||
render(<SelectDropdown value="option1" options={options} onChange={onChangeMock} disabled={true} />)
|
||||
|
||||
const trigger = screen.getByTestId("dropdown-trigger")
|
||||
expect(trigger).toHaveAttribute("disabled")
|
||||
})
|
||||
|
||||
it("renders with width: 100% for proper sizing", () => {
|
||||
render(<SelectDropdown value="option1" options={options} onChange={onChangeMock} />)
|
||||
|
||||
const trigger = screen.getByTestId("dropdown-trigger")
|
||||
expect(trigger).toHaveStyle("width: 100%")
|
||||
})
|
||||
|
||||
it("passes the selected value to the trigger", () => {
|
||||
const { rerender } = render(<SelectDropdown value="option1" options={options} onChange={onChangeMock} />)
|
||||
|
||||
// Check initial render using testId to be specific
|
||||
const trigger = screen.getByTestId("dropdown-trigger")
|
||||
expect(trigger).toHaveTextContent("Option 1")
|
||||
|
||||
// Rerender with a different value
|
||||
rerender(<SelectDropdown value="option3" options={options} onChange={onChangeMock} />)
|
||||
|
||||
// Check updated render
|
||||
expect(trigger).toHaveTextContent("Option 3")
|
||||
})
|
||||
|
||||
it("applies custom className to trigger when provided", () => {
|
||||
render(
|
||||
<SelectDropdown
|
||||
value="option1"
|
||||
options={options}
|
||||
onChange={onChangeMock}
|
||||
triggerClassName="custom-trigger-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByTestId("dropdown-trigger")
|
||||
expect(trigger.classList.toString()).toContain("custom-trigger-class")
|
||||
})
|
||||
|
||||
// Tests for the new functionality
|
||||
describe("Option types", () => {
|
||||
it("renders separator options correctly", () => {
|
||||
const optionsWithTypedSeparator = [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "sep-1", label: "Separator", type: DropdownOptionType.SEPARATOR },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
]
|
||||
|
||||
render(<SelectDropdown value="option1" options={optionsWithTypedSeparator} onChange={onChangeMock} />)
|
||||
|
||||
// Check for separator
|
||||
const separators = screen.getAllByTestId("dropdown-separator")
|
||||
expect(separators.length).toBe(1)
|
||||
})
|
||||
|
||||
it("renders string separator (backward compatibility) correctly", () => {
|
||||
const optionsWithStringSeparator = [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "sep-1", label: "────", disabled: true },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
]
|
||||
|
||||
render(<SelectDropdown value="option1" options={optionsWithStringSeparator} onChange={onChangeMock} />)
|
||||
|
||||
// Check for separator
|
||||
const separators = screen.getAllByTestId("dropdown-separator")
|
||||
expect(separators.length).toBe(1)
|
||||
})
|
||||
|
||||
it("renders shortcut options correctly", () => {
|
||||
const shortcutText = "Ctrl+K"
|
||||
const optionsWithShortcut = [
|
||||
{ value: "shortcut", label: shortcutText, type: DropdownOptionType.SHORTCUT },
|
||||
{ value: "option1", label: "Option 1" },
|
||||
]
|
||||
|
||||
render(
|
||||
<SelectDropdown
|
||||
value="option1"
|
||||
options={optionsWithShortcut}
|
||||
onChange={onChangeMock}
|
||||
shortcutText={shortcutText}
|
||||
/>,
|
||||
)
|
||||
|
||||
// The shortcut text should be rendered as a div, not a dropdown item
|
||||
expect(screen.queryByText(shortcutText)).toBeInTheDocument()
|
||||
const dropdownItems = screen.getAllByTestId("dropdown-item")
|
||||
expect(dropdownItems.length).toBe(1) // Only one regular option
|
||||
})
|
||||
|
||||
it("handles action options correctly", () => {
|
||||
const optionsWithAction = [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "settingsButtonClicked", label: "Settings", type: DropdownOptionType.ACTION },
|
||||
]
|
||||
|
||||
render(<SelectDropdown value="option1" options={optionsWithAction} onChange={onChangeMock} />)
|
||||
|
||||
// Get all dropdown items
|
||||
const dropdownItems = screen.getAllByTestId("dropdown-item")
|
||||
|
||||
// Click the action item
|
||||
fireEvent.click(dropdownItems[1])
|
||||
|
||||
// Check that postMessage was called with the correct action
|
||||
expect(postMessageMock).toHaveBeenCalledWith({
|
||||
type: "action",
|
||||
action: "settingsButtonClicked",
|
||||
})
|
||||
|
||||
// The onChange callback should not be called for action items
|
||||
expect(onChangeMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("only treats options with explicit ACTION type as actions", () => {
|
||||
const optionsForTest = [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
// This should be treated as a regular option despite the -action suffix
|
||||
{ value: "settings-action", label: "Regular option with action suffix" },
|
||||
// This should be treated as an action
|
||||
{ value: "settingsButtonClicked", label: "Settings", type: DropdownOptionType.ACTION },
|
||||
]
|
||||
|
||||
render(<SelectDropdown value="option1" options={optionsForTest} onChange={onChangeMock} />)
|
||||
|
||||
// Get all dropdown items
|
||||
const dropdownItems = screen.getAllByTestId("dropdown-item")
|
||||
|
||||
// Click the second option (with action suffix but no ACTION type)
|
||||
fireEvent.click(dropdownItems[1])
|
||||
|
||||
// Should trigger onChange, not postMessage
|
||||
expect(onChangeMock).toHaveBeenCalledWith("settings-action")
|
||||
expect(postMessageMock).not.toHaveBeenCalled()
|
||||
|
||||
// Reset mocks
|
||||
onChangeMock.mockReset()
|
||||
postMessageMock.mockReset()
|
||||
|
||||
// Click the third option (ACTION type)
|
||||
fireEvent.click(dropdownItems[2])
|
||||
|
||||
// Should trigger postMessage with "settingsButtonClicked", not onChange
|
||||
expect(postMessageMock).toHaveBeenCalledWith({
|
||||
type: "action",
|
||||
action: "settingsButtonClicked",
|
||||
})
|
||||
expect(onChangeMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("calls onChange for regular menu items", () => {
|
||||
render(<SelectDropdown value="option1" options={options} onChange={onChangeMock} />)
|
||||
|
||||
// Get all dropdown items
|
||||
const dropdownItems = screen.getAllByTestId("dropdown-item")
|
||||
|
||||
// Click the second option (index 1)
|
||||
fireEvent.click(dropdownItems[1])
|
||||
|
||||
// Check that onChange was called with the correct value
|
||||
expect(onChangeMock).toHaveBeenCalledWith("option2")
|
||||
|
||||
// postMessage should not be called for regular items
|
||||
expect(postMessageMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -13,3 +13,4 @@ export * from "./separator"
|
|||
export * from "./slider"
|
||||
export * from "./textarea"
|
||||
export * from "./tooltip"
|
||||
export * from "./select-dropdown"
|
||||
|
|
|
|||
157
webview-ui/src/components/ui/select-dropdown.tsx
Normal file
157
webview-ui/src/components/ui/select-dropdown.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import * as React from "react"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "./dropdown-menu"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Constants for option types
|
||||
export enum DropdownOptionType {
|
||||
ITEM = "item",
|
||||
SEPARATOR = "separator",
|
||||
SHORTCUT = "shortcut",
|
||||
ACTION = "action",
|
||||
}
|
||||
export interface DropdownOption {
|
||||
value: string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
type?: DropdownOptionType // Optional type to specify special behaviors
|
||||
}
|
||||
|
||||
export interface SelectDropdownProps {
|
||||
value: string
|
||||
options: DropdownOption[]
|
||||
onChange: (value: string) => void
|
||||
disabled?: boolean
|
||||
title?: string
|
||||
className?: string
|
||||
triggerClassName?: string
|
||||
contentClassName?: string
|
||||
sideOffset?: number
|
||||
align?: "start" | "center" | "end"
|
||||
shouldShowCaret?: boolean
|
||||
placeholder?: string
|
||||
shortcutText?: string
|
||||
}
|
||||
|
||||
export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownMenuTrigger>, SelectDropdownProps>(
|
||||
(
|
||||
{
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
disabled = false,
|
||||
title = "",
|
||||
className = "",
|
||||
triggerClassName = "",
|
||||
contentClassName = "",
|
||||
sideOffset = 4,
|
||||
align = "start",
|
||||
shouldShowCaret = true,
|
||||
placeholder = "",
|
||||
shortcutText = "",
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
// Find the selected option label
|
||||
const selectedOption = options.find((option) => option.value === value)
|
||||
const displayText = selectedOption?.label || placeholder || ""
|
||||
|
||||
// Handle menu item click
|
||||
const handleSelect = (option: DropdownOption) => {
|
||||
// Check if this is an action option by its explicit type
|
||||
if (option.type === DropdownOptionType.ACTION) {
|
||||
window.postMessage({
|
||||
type: "action",
|
||||
action: option.value,
|
||||
})
|
||||
return
|
||||
}
|
||||
onChange(option.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 relative whitespace-nowrap rounded pr-1.5 py-1.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-vscode-focusBorder",
|
||||
"bg-transparent border-none text-vscode-foreground w-auto",
|
||||
disabled ? "opacity-50 cursor-not-allowed" : "opacity-80 cursor-pointer hover:opacity-100",
|
||||
triggerClassName,
|
||||
)}
|
||||
style={{
|
||||
width: "100%", // Take full width of parent
|
||||
minWidth: "0",
|
||||
maxWidth: "100%",
|
||||
}}>
|
||||
{shouldShowCaret && (
|
||||
<div className="pointer-events-none opacity-80 flex-shrink-0">
|
||||
<svg
|
||||
fill="none"
|
||||
height="10"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="10">
|
||||
<polyline points="18 15 12 9 6 15" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<span className="truncate">{displayText}</span>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border z-50",
|
||||
contentClassName,
|
||||
)}>
|
||||
{options.map((option, index) => {
|
||||
// Handle separator type
|
||||
if (option.type === DropdownOptionType.SEPARATOR || option.label.includes("────")) {
|
||||
return <DropdownMenuSeparator key={`sep-${index}`} />
|
||||
}
|
||||
|
||||
// Handle shortcut text type (disabled label for keyboard shortcuts)
|
||||
if (
|
||||
option.type === DropdownOptionType.SHORTCUT ||
|
||||
(option.disabled && shortcutText && option.label.includes(shortcutText))
|
||||
) {
|
||||
return (
|
||||
<div key={`label-${index}`} className="px-2 py-1.5 text-xs opacity-50">
|
||||
{option.label}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Regular menu items
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={`item-${option.value}`}
|
||||
disabled={option.disabled}
|
||||
className={cn(
|
||||
"cursor-pointer text-xs focus:bg-vscode-list-hoverBackground focus:text-vscode-list-hoverForeground",
|
||||
option.value === value && "bg-vscode-list-focusBackground",
|
||||
)}
|
||||
onClick={() => handleSelect(option)}>
|
||||
{option.label}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
SelectDropdown.displayName = "SelectDropdown"
|
||||
Loading…
Add table
Reference in a new issue