+
{
+ if (!disabled) inputRef.current?.focus();
+ }}
+ className={cn(
+ 'relative w-full rounded-md border border-input bg-background transition-colors duration-150 focus-within:ring-2 focus-within:ring-ring hover:border-muted-foreground',
+ disabled && 'cursor-not-allowed opacity-50',
+ multiple ? 'flex flex-wrap items-center gap-1.5 p-1.5 min-h-[38px] pe-14' : 'flex items-center',
+ )}
+ >
+ {multiple &&
+ selectedOptions.map((opt) => (
+
+ {opt.label}
+ {!disabled && (
+
+ )}
+
+ ))}
+
+
= 0 ? `${listboxId}-opt-${highlightedIndex}` : undefined}
+ value={query}
+ placeholder={multiple && selectedValues.length > 0 ? '' : placeholder}
+ onChange={(e) => {
+ setQuery(e.target.value);
+ setIsTyping(true);
+ if (!isOpen) setIsOpen(true);
+ }}
+ onFocus={() => {
+ if (!disabled && !isOpen) {
+ setIsOpen(true);
+ setIsTyping(false);
+ }
+ }}
+ onClick={() => {
+ if (!disabled && !isOpen) {
+ setIsOpen(true);
+ setIsTyping(false);
+ }
+ }}
+ onKeyDown={handleKeyDown}
+ className={cn(
+ multiple
+ ? 'flex-1 min-w-[80px] bg-transparent border-0 p-0 text-sm text-foreground placeholder:text-muted-foreground outline-none focus:ring-0'
+ : cn(
+ 'w-full rounded-md border-0 bg-transparent ps-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground outline-none',
+ clearable && hasValue ? 'pe-14' : 'pe-8',
+ ),
+ )}
+ />
+
+ {clearable && hasValue && !disabled && (
+
+ )}
+
+
+
+
+ {isOpen && !isHostFloating && (
+
+ {filteredOptions.length === 0 ? (
+
+ No results
+
+ ) : (
+ filteredOptions.map((opt, i) => {
+ const isSelected = multiple ? selectedValues.includes(opt.value) : opt.value === singleValue;
+ const isHighlighted = highlightedIndex === i;
+ return (
+
{ optionRefs.current[i] = el; }}
+ id={`${listboxId}-opt-${i}`}
+ role="option"
+ aria-selected={isSelected}
+ aria-disabled={opt.disabled}
+ onMouseDown={(e) => e.preventDefault()}
+ onClick={() => selectOption(opt)}
+ onMouseEnter={() => setHighlightedIndex(i)}
+ className={cn(
+ 'relative flex cursor-pointer select-none items-center justify-between rounded-sm px-2 py-1.5 text-sm transition-colors',
+ isHighlighted ? 'bg-accent text-accent-foreground' : 'text-foreground',
+ isSelected && 'font-medium',
+ opt.disabled && 'pointer-events-none opacity-50',
+ )}
+ >
+ {opt.label}
+ {isSelected && }
+
+ );
+ })
+ )}
+
+ )}
+
+ );
+}
+
export interface KitCheckboxProps {
checked: boolean;
onChange?: (checked: boolean) => void;
diff --git a/lib/plugin-sandbox/ui-kit/floating-dropdown-bridge.ts b/lib/plugin-sandbox/ui-kit/floating-dropdown-bridge.ts
new file mode 100644
index 000000000..ec93d1d76
--- /dev/null
+++ b/lib/plugin-sandbox/ui-kit/floating-dropdown-bridge.ts
@@ -0,0 +1,82 @@
+// Bridge between Combobox in `@plugin-host/ui` and the sandbox runtime.
+// When running inside a host-framed sandbox iframe, Combobox delegates its
+// floating dropdown rendering to the host so the dropdown menu floats
+// outside the popup modal boundary without causing the popup to scroll.
+
+import type {
+ FloatingDropdownOpenMsg,
+ FloatingDropdownUpdateMsg,
+ FloatingDropdownOption,
+ FloatingDropdownRect,
+} from '../protocol';
+
+export type { FloatingDropdownOption, FloatingDropdownRect };
+
+export type FloatingDropdownHostEvent =
+ | { type: 'select'; option: FloatingDropdownOption }
+ | { type: 'highlight'; index: number }
+ | { type: 'dismiss' };
+
+type DropdownListener = (event: FloatingDropdownHostEvent) => void;
+
+let hostSender: ((msg: any) => void) | null = null;
+let hostAvailableCheck: (() => boolean) | null = null;
+const listeners = new Map