From ec697a438c9e3d754d1088ff44c743e7e5cd32da Mon Sep 17 00:00:00 2001 From: Jinpy Date: Fri, 25 Sep 2026 09:42:22 +0800 Subject: [PATCH] fix(plugins): keep dock plus picker inside window and open downward --- .../components/plugins/PluginBottomDock.vue | 93 ++++++++++--------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/apps/desktop/src/components/plugins/PluginBottomDock.vue b/apps/desktop/src/components/plugins/PluginBottomDock.vue index ec40eaaac2..3d6145630c 100644 --- a/apps/desktop/src/components/plugins/PluginBottomDock.vue +++ b/apps/desktop/src/components/plugins/PluginBottomDock.vue @@ -9,6 +9,7 @@ import { useI18n } from "vue-i18n"; import { ChevronDown, ChevronUp, Maximize2, Minimize2, Plus, X } from "@lucide/vue"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import PluginIcon from "@/components/plugins/PluginIcon.vue"; import PluginWorkbenchHost from "@/components/plugins/PluginWorkbenchHost.vue"; import { useConnectionStore } from "@/stores/connectionStore"; @@ -370,9 +371,14 @@ function onResizeHandlePointerDown(event: PointerEvent) { startResize(event); } -// "+" picker menu: anchored below the + button, bottom-stuck inside the dock, -// growing upward with content (capped by the dock body), closed by selecting -// an item or clicking anywhere else. +// "+" picker menu (host DropdownMenu, reka-ui): collision-aware placement โ€” +// the menu flips/shifts to stay inside the window instead of running off the +// edge when the "+" sits near the strip's end. open is controlled so opening +// can run side effects (launch options fetch, panel html warm), and closing +// stays driven by the menu itself (outside pointerdown incl. the trigger, +// Escape, item selection). The window-blur closer covers the cross-document +// iframe case: a click inside a plugin terminal never reaches this document, +// it only moves focus out of it. const plusOpen = ref(false); // Generic list filter for the "+" picker: purely client-side label matching so // any plugin's long option/target list stays usable without the host knowing @@ -385,16 +391,21 @@ function plusMatches(label: string): boolean { return !plusQuery.value || label.toLowerCase().includes(plusQuery.value); } const visibleLaunchOptions = computed(() => launchOptionEntries.value.filter((option) => plusMatches(option.label))); -const visibleConnectionTargets = computed(() => connectionTargets.value.filter((target) => plusMatches(target.label))); -const plusRoot = ref(); -function togglePlusMenu() { - plusOpen.value = !plusOpen.value; +const visibleConnectionTargets = computed(() => connectionTargets.value.filter((target) => plusMatches(optionLabel(target)))); +function optionLabel(target: { label: string }): string { + return target.label; +} +function setPlusOpen(open: boolean) { + plusOpen.value = open; plusFilter.value = ""; - if (plusOpen.value) { + if (open) { void loadLaunchOptions(); warmActivePluginPanelHtml(); } } +function closePlusMenu() { + plusOpen.value = false; +} // Hide the first-open html pipeline under the time the user spends reading the // "+" picker: the read/decode/inline of a multi-megabyte ui build runs while @@ -407,28 +418,12 @@ function warmActivePluginPanelHtml() { if (!plugin) return; void getOrLoadPluginUiHtml(`${plugin.manifest.id}:${plugin.manifest.version}`, plugin.manifest.id).catch(() => undefined); } -function closePlusMenu() { - plusOpen.value = false; -} -const onPlusMenuOutsidePointerDown = (event: PointerEvent) => { - const root = plusRoot.value; - if (plusOpen.value && root && !root.contains(event.target as Node)) closePlusMenu(); -}; -window.addEventListener("pointerdown", onPlusMenuOutsidePointerDown, true); // Plugin terminal panels live in cross-document iframes: a click inside one -// never reaches the host window's pointerdown closer above, but it does move -// focus out of the host document โ€” close on blur. Escape covers keyboard users. +// never reaches this document (no outside-pointerdown, no Escape keyup), but +// it does move focus out of the host document โ€” close on blur. const onPlusMenuWindowBlur = () => closePlusMenu(); -const onPlusMenuKeydown = (event: KeyboardEvent) => { - if (plusOpen.value && event.key === "Escape") closePlusMenu(); -}; window.addEventListener("blur", onPlusMenuWindowBlur); -window.addEventListener("keydown", onPlusMenuKeydown, true); -onScopeDispose(() => { - window.removeEventListener("pointerdown", onPlusMenuOutsidePointerDown, true); - window.removeEventListener("blur", onPlusMenuWindowBlur); - window.removeEventListener("keydown", onPlusMenuKeydown, true); -}); +onScopeDispose(() => window.removeEventListener("blur", onPlusMenuWindowBlur));