feat(sidebar): support hierarchical batch connection moves

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-08-22 19:18:11 +08:00
1 parent 36c9500a4e
commit 904efff6b3
10 files changed
+105 -38

No files matched your search

@@ -282,7 +282,7 @@ onBeforeUnmount(() => {
<div v-if="item.separator" class="-mx-1 my-1 flex items-center px-1">
<div class="h-px flex-1 bg-border/70" />
</div>
<button v-else :disabled="itemIsDisabled(item)" :class="[...itemButtonClass(item.variant), activeSubmenuTriggerClass(item, index)]" @click="handleItemClick(item)" @mouseenter="(e) => onItemMouseEnter(index, e)" @mouseleave="onItemMouseLeave">
<button v-else :disabled="itemIsDisabled(item)" :title="item.title" :class="[...itemButtonClass(item.variant), activeSubmenuTriggerClass(item, index)]" @click="handleItemClick(item)" @mouseenter="(e) => onItemMouseEnter(index, e)" @mouseleave="onItemMouseLeave">
<span class="flex size-4 shrink-0 items-center justify-center">
<Check v-if="item.checked" class="size-4 text-primary" />
<component :is="item.icon" v-else-if="item.icon" :class="['size-4', item.iconClass]" />
@@ -313,7 +313,7 @@ onBeforeUnmount(() => {
<div v-if="child.separator" class="-mx-1 my-1 flex items-center px-1">
<div class="h-px flex-1 bg-border/70" />
</div>
<button v-else :disabled="itemIsDisabled(child)" :class="itemButtonClass(child.variant)" @click="handleSubItemClick(child)">
<button v-else :disabled="itemIsDisabled(child)" :title="child.title" :class="itemButtonClass(child.variant)" :style="{ paddingInlineStart: `${0.5 + (child.indentLevel ?? 0) * 0.75}rem` }" @click="handleSubItemClick(child)">
<span class="flex size-4 shrink-0 items-center justify-center">
<Check v-if="child.checked" class="size-4 text-primary" />
<component :is="child.icon" v-else-if="child.icon" :class="['size-4', child.iconClass]" />
@@ -11,6 +11,7 @@ export interface LightDropdownItem {
leadingText?: string;
disabled?: boolean;
separatorBefore?: boolean;
indentLevel?: number;
}
const props = withDefaults(
@@ -83,13 +84,20 @@ const menuStyle = computed(() => ({
left: `${x.value}px`,
top: `${y.value}px`,
minWidth: props.matchTriggerWidth ? `${minWidth.value}px` : undefined,
maxHeight: "min(420px, calc(100vh - 16px))",
}));
function onScroll(event: Event) {
const target = event.target;
if (target instanceof Node && menuRef.value?.contains(target)) return;
close();
}
function close() {
open.value = false;
document.removeEventListener("pointerdown", onOutsidePointerDown, true);
document.removeEventListener("keydown", onKeydown);
window.removeEventListener("scroll", close, true);
window.removeEventListener("scroll", onScroll, true);
window.removeEventListener("resize", close);
}
@@ -123,7 +131,7 @@ function openMenu() {
nextTick(fitPositionToViewport);
document.addEventListener("pointerdown", onOutsidePointerDown, true);
document.addEventListener("keydown", onKeydown);
window.addEventListener("scroll", close, true);
window.addEventListener("scroll", onScroll, true);
window.addEventListener("resize", close);
}
@@ -169,7 +177,7 @@ onBeforeUnmount(close);
<ChevronDown v-if="showChevron" class="h-3 w-3 opacity-50" />
</button>
<Teleport to="body">
<div v-if="open" ref="menuRef" class="fixed z-50 min-w-32 rounded-md p-1 cn-menu-translucent text-popover-foreground" :class="contentClass" :style="menuStyle" role="menu">
<div v-if="open" ref="menuRef" class="fixed z-50 min-w-32 overflow-x-hidden overflow-y-auto rounded-md p-1 cn-menu-translucent text-popover-foreground" :class="contentClass" :style="menuStyle" role="menu">
<div v-if="label" :class="labelClass">{{ label }}</div>
<div v-if="label" class="bg-border -mx-1 my-1 h-px" />
<template v-for="item in items" :key="item.value">
@@ -180,6 +188,7 @@ onBeforeUnmount(close);
:class="[itemClass, highlightSelected && isItemSelected(item) ? selectedItemClass : '']"
:disabled="item.disabled"
:title="item.title"
:style="{ paddingInlineStart: `${0.375 + (item.indentLevel ?? 0) * 0.75}rem` }"
role="menuitem"
@click="selectItem(item)"
>
@@ -7,6 +7,8 @@ export interface ContextMenuItem {
separator?: boolean;
icon?: Component;
iconClass?: string;
title?: string;
indentLevel?: number;
checked?: boolean;
// Raw shortcut syntax such as `Mod+C` or `Shift+Alt+U`; display formatting stays in this component.
shortcut?: string;