chore(shortcuts): remove the never-wired shortcut organizer

ui_shortcut_organizer.js and shortcut_layout.js only referenced each other;
the bottom bar is the fixed one built in ui_toolbox.js. Also drops their
eleven locale keys and marks the customizable-shortcuts plan as abandoned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DemonGatanjieu
2026-09-26 17:57:41 +08:00
co-authored by Claude Opus 5.5
parent 55208061a8
commit bd7fb8beaa
5 changed files with 2 additions and 705 deletions
+1 -1
View File
@@ -83,7 +83,7 @@
在阶段 1 稳定后,从 `createDOM()` 提取:
- `ui_settings_hub.js`:设置中心与模型显示设置。接口覆盖 open/close/refreshLanguage,保留原来的设置来源、键名和生效时机。
- `ui_toolbox.js`:工具箱内容、开关和底部按钮装配;复用 `tool_registry.js`、`shortcut_layout.js`、`sidebar_actions.js`、`ui_shortcut_organizer.js`。按**当前代码**保持固定栏与工具过滤规则,不顺便实施旧计划中的另一种交互。
- `ui_toolbox.js`:工具箱内容、开关和底部按钮装配;复用 `tool_registry.js`、`sidebar_actions.js`。按**当前代码**保持固定栏与工具过滤规则,不顺便实施旧计划中的另一种交互。
- `ui_browser_navigation.js`:主面板切换与返回协调。先收拢现有 `hideAllPanels/closeWorkspace` 相关调用,不引入新的路由系统;笔记模块最终不再独占其他工作台的公共关闭逻辑。
- 窗口拖动、缩放、停靠先保留在外壳;仍形成独立大段时再提取 `browser_window.js`,复用生命周期工具,不改变尺寸偏好。
+1 -1
View File
@@ -1,6 +1,6 @@
# 工具箱与快捷栏自定义:交给 Gemini 的实施计划
状态:待实施的产品与工程计划,不代表当前版本已经具备以下功能。
状态:已放弃。当前是固定快捷栏(见 `ui_toolbox.js`);按本计划写的 `shortcut_layout.js`、`ui_shortcut_organizer.js` 从未接入,已删除。只作历史参考,不要照此实施。
基线:`77ae7ce88`;开工时以实际 Git 差异和现有代码为准,不回退后续工作。
## 目标与范围
-22
View File
@@ -77,17 +77,6 @@ export const i18n = {
versionError_unknown_tag: "这不是已发布的版本。",
versionError_no_previous: "没有可以撤销的切换。",
versionError_no_default_branch: "无法确定默认分支。",
shortcutBarPinned: "已固定",
shortcutBarPin: "固定到快捷栏",
shortcutBarUnpin: "从快捷栏移除",
shortcutBarMoveLeft: "向左移动",
shortcutBarMoveRight: "向右移动",
shortcutBarCapacityFull: "快捷栏已满,请先将一个工具移回工具箱",
shortcutBarReset: "恢复默认快捷栏",
shortcutBarResetConfirm: "确定要将快捷栏恢复为默认布局吗?",
shortcutDropHint: "拖到下方,固定常用工具;拖回这里,收起快捷入口",
shortcutDropRelease: "松开,从快捷栏移除",
shortcutBarAnchorTip: "固定入口,不可移动",
updateGuideStartTour: "🎯 界面按键遮罩导览",
updateGuideTourBanner: "💡 想在主界面实地体验?点击开启按键遮罩导览 ›",
materialDragGlobalHint: "💡 提示:按住卡片直接拖到画布节点上注入参数,拖到空白处载入工作流",
@@ -1810,17 +1799,6 @@ export const i18n = {
versionError_unknown_tag: "That is not a published version.",
versionError_no_previous: "There is no switch to undo.",
versionError_no_default_branch: "Couldn't determine the default branch.",
shortcutBarPinned: "Pinned",
shortcutBarPin: "Pin to shortcuts",
shortcutBarUnpin: "Remove from shortcuts",
shortcutBarMoveLeft: "Move left",
shortcutBarMoveRight: "Move right",
shortcutBarCapacityFull: "Shortcut bar is full. Move a tool back to Toolbox first",
shortcutBarReset: "Reset shortcut bar",
shortcutBarResetConfirm: "Reset the shortcut bar to default layout?",
shortcutDropHint: "Drag down to pin shortcuts; drag back here to unpin",
shortcutDropRelease: "Release to remove from shortcuts",
shortcutBarAnchorTip: "Fixed anchor, cannot be moved",
updateGuideStartTour: "🎯 Spotlight Tour",
updateGuideTourBanner: "💡 Prefer to see them in action? Start Spotlight Tour ›",
materialDragGlobalHint: "💡 Tip: Drag cards onto canvas nodes to inject values, or onto empty canvas to load the workflow",
-178
View File
@@ -1,178 +0,0 @@
/**
* shortcut_layout.js - State and Persistence for Customizable Shortcut Bar
*
* Implements schema validation, capacity limits, pinning, unpinning,
* reordering and reset logic with localStorage persistence.
*/
import { CATALOG_TOOLS } from './tool_registry.js';
export const SHORTCUT_STORAGE_KEY = 'anomalous_shortcut_layout_v1';
export const SHORTCUT_SCHEMA_VERSION = 1;
export const MAX_PINNED_SHORTCUTS = 4;
export const DEFAULT_PINNED_SHORTCUTS = Object.freeze([
'scan',
'doctor',
'assistant',
'materials',
]);
// In-memory fallback if localStorage fails or is blocked
let sessionMemoryLayout = null;
const VALID_CATALOG_IDS = new Set(CATALOG_TOOLS.map(t => t.id));
export function isValidCatalogToolId(id) {
return VALID_CATALOG_IDS.has(id);
}
/**
* Normalizes an array of tool IDs:
* - Drops invalid IDs, anchor IDs and non-strings
* - Deduplicates preserving first occurrence
* - Truncates to MAX_PINNED_SHORTCUTS
*/
export function normalizePinnedIds(ids) {
if (!Array.isArray(ids)) return [...DEFAULT_PINNED_SHORTCUTS];
const seen = new Set();
const result = [];
for (const id of ids) {
if (typeof id === 'string' && VALID_CATALOG_IDS.has(id) && !seen.has(id)) {
seen.add(id);
result.push(id);
if (result.length >= MAX_PINNED_SHORTCUTS) break;
}
}
return result;
}
/**
* Load shortcut layout from storage.
* Explicit empty array is valid and preserved.
*/
export function loadShortcutLayout(storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
if (sessionMemoryLayout !== null && !storage) {
return [...sessionMemoryLayout];
}
if (!storage) {
return sessionMemoryLayout !== null ? [...sessionMemoryLayout] : [...DEFAULT_PINNED_SHORTCUTS];
}
try {
const raw = storage.getItem(SHORTCUT_STORAGE_KEY);
if (raw === null || raw === undefined) {
return [...DEFAULT_PINNED_SHORTCUTS];
}
const parsed = JSON.parse(raw);
if (!parsed || parsed.schemaVersion !== SHORTCUT_SCHEMA_VERSION || !Array.isArray(parsed.pinned)) {
return [...DEFAULT_PINNED_SHORTCUTS];
}
return normalizePinnedIds(parsed.pinned);
} catch (err) {
console.warn('[AMB] Failed to read shortcut layout from storage, resetting to defaults:', err);
sessionMemoryLayout = [...DEFAULT_PINNED_SHORTCUTS];
return [...DEFAULT_PINNED_SHORTCUTS];
}
}
/**
* Persist shortcut layout to storage and memory.
*/
export function saveShortcutLayout(pinnedIds, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
const normalized = normalizePinnedIds(pinnedIds);
sessionMemoryLayout = [...normalized];
if (!storage) {
return { success: true, layout: normalized, memoryOnly: true };
}
try {
const payload = JSON.stringify({
schemaVersion: SHORTCUT_SCHEMA_VERSION,
pinned: normalized,
});
storage.setItem(SHORTCUT_STORAGE_KEY, payload);
return { success: true, layout: normalized, memoryOnly: false };
} catch (err) {
console.warn('[AMB] Failed to write shortcut layout to storage, saved to session memory:', err);
return { success: true, layout: normalized, memoryOnly: true, error: err };
}
}
/**
* Pin a tool to the shortcut bar.
* If already pinned, reorder to targetIndex.
* If not pinned and at capacity, rejects with 'capacity_full'.
*/
export function pinTool(toolId, targetIndex = -1, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
if (!isValidCatalogToolId(toolId)) {
return { success: false, reason: 'unknown_tool', layout: loadShortcutLayout(storage) };
}
const current = loadShortcutLayout(storage);
const existingIndex = current.indexOf(toolId);
if (existingIndex !== -1) {
// Already pinned: reorder to target position
current.splice(existingIndex, 1);
const insertAt = (targetIndex >= 0 && targetIndex <= current.length) ? targetIndex : current.length;
current.splice(insertAt, 0, toolId);
saveShortcutLayout(current, storage);
return { success: true, reordered: true, layout: current };
}
if (current.length >= MAX_PINNED_SHORTCUTS) {
return { success: false, reason: 'capacity_full', layout: current };
}
const insertAt = (targetIndex >= 0 && targetIndex <= current.length) ? targetIndex : current.length;
current.splice(insertAt, 0, toolId);
saveShortcutLayout(current, storage);
return { success: true, reordered: false, layout: current };
}
/**
* Unpin a tool from the shortcut bar (returns it to toolbox only).
*/
export function unpinTool(toolId, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
const current = loadShortcutLayout(storage);
const index = current.indexOf(toolId);
if (index === -1) {
return { success: true, changed: false, layout: current };
}
current.splice(index, 1);
saveShortcutLayout(current, storage);
return { success: true, changed: true, layout: current };
}
/**
* Reorder an item in the shortcut bar.
*/
export function reorderShortcut(fromIndex, toIndex, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
const current = loadShortcutLayout(storage);
if (fromIndex < 0 || fromIndex >= current.length || toIndex < 0 || toIndex >= current.length || fromIndex === toIndex) {
return { success: false, layout: current };
}
const [item] = current.splice(fromIndex, 1);
current.splice(toIndex, 0, item);
saveShortcutLayout(current, storage);
return { success: true, layout: current };
}
/**
* Reset shortcut layout to defaults.
*/
export function resetShortcutLayout(storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
saveShortcutLayout(DEFAULT_PINNED_SHORTCUTS, storage);
return { success: true, layout: [...DEFAULT_PINNED_SHORTCUTS] };
}
/**
* Check if a tool is currently pinned.
*/
export function isToolPinned(toolId, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
const current = loadShortcutLayout(storage);
return current.includes(toolId);
}
-503
View File
@@ -1,503 +0,0 @@
/**
* ui_shortcut_organizer.js - Drag-and-drop & Menu Management for Shortcut Bar & Toolbox
*
* Implements:
* 1. Pointer drag-and-drop between Toolbox and Shortcut Bar (with 6px deadzone).
* 2. 350ms auto-expand when dragging over closed Toolbox button.
* 3. Drop indicators and visual drop-zones.
* 4. Capacity limit feedback (max 4 pinned items).
* 5. Accessible context / "More" menus for organizing without mouse drag.
* 6. Full cancel/cleanup on Escape, pointercancel, or window blur.
*/
import { translate as t } from './locales.js';
import { getToolDefinition, CATALOG_TOOLS, FIXED_ANCHORS } from './tool_registry.js';
import {
loadShortcutLayout,
pinTool,
unpinTool,
reorderShortcut,
resetShortcutLayout,
isToolPinned,
MAX_PINNED_SHORTCUTS,
} from './shortcut_layout.js';
import { setDraggingState, hideTooltipImmediately } from './sidebar_actions.js';
import { showWorkbenchToast } from './ui_prompt_toast.js';
let activeDrag = null;
let autoOpenTimer = null;
let activeMenuEl = null;
function closeActiveMenu() {
if (activeMenuEl) {
activeMenuEl.remove();
activeMenuEl = null;
}
}
document.addEventListener('mousedown', (e) => {
if (activeMenuEl && !activeMenuEl.contains(e.target)) {
closeActiveMenu();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (activeDrag) {
cancelCurrentDrag();
}
closeActiveMenu();
}
});
export function isOrganizingDragActive() {
return Boolean(activeDrag && activeDrag.isDragging);
}
function showCapacityFullToast() {
showWorkbenchToast(t('shortcutBarCapacityFull'));
}
/**
* Creates and shows a context dropdown menu.
*/
function showDropdownMenu(items, x, y) {
closeActiveMenu();
if (!items || items.length === 0) return;
const menu = document.createElement('div');
menu.className = 'anomalous-shortcut-context-menu';
menu.style.position = 'fixed';
menu.style.zIndex = '1000002';
menu.style.display = 'flex';
menu.style.flexDirection = 'column';
menu.style.minWidth = '140px';
menu.style.padding = '4px';
menu.style.borderRadius = '8px';
menu.style.background = 'rgba(20, 22, 28, 0.96)';
menu.style.backdropFilter = 'blur(16px)';
menu.style.border = '1px solid rgba(255, 255, 255, 0.14)';
menu.style.boxShadow = '0 12px 36px rgba(0, 0, 0, 0.7), 0 0 1px 1px rgba(255, 255, 255, 0.1)';
for (const item of items) {
if (item.separator) {
const sep = document.createElement('div');
sep.style.height = '1px';
sep.style.margin = '4px 2px';
sep.style.background = 'rgba(255, 255, 255, 0.08)';
menu.appendChild(sep);
continue;
}
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = item.label;
btn.disabled = Boolean(item.disabled);
btn.style.display = 'flex';
btn.style.alignItems = 'center';
btn.style.gap = '8px';
btn.style.width = '100%';
btn.style.padding = '6px 10px';
btn.style.border = 'none';
btn.style.borderRadius = '5px';
btn.style.background = 'transparent';
btn.style.color = item.disabled ? '#64748b' : (item.danger ? '#f87171' : '#e2e8f0');
btn.style.fontSize = '12px';
btn.style.cursor = item.disabled ? 'not-allowed' : 'pointer';
btn.style.textAlign = 'left';
btn.style.transition = 'all 0.12s ease';
if (!item.disabled) {
btn.onmouseover = () => {
btn.style.background = 'rgba(255, 255, 255, 0.08)';
btn.style.color = '#ffffff';
};
btn.onmouseout = () => {
btn.style.background = 'transparent';
btn.style.color = item.danger ? '#f87171' : '#e2e8f0';
};
btn.onclick = (e) => {
e.stopPropagation();
closeActiveMenu();
item.action();
};
}
menu.appendChild(btn);
}
document.body.appendChild(menu);
// Bounding clamp
const menuRect = menu.getBoundingClientRect();
let left = x;
let top = y;
if (left + menuRect.width > window.innerWidth - 8) {
left = window.innerWidth - menuRect.width - 8;
}
if (left < 8) left = 8;
if (top + menuRect.height > window.innerHeight - 8) {
top = y - menuRect.height;
}
if (top < 8) top = 8;
menu.style.left = `${Math.round(left)}px`;
menu.style.top = `${Math.round(top)}px`;
activeMenuEl = menu;
}
export function openToolboxCardMenu(toolId, targetEl, onLayoutChange) {
if (!targetEl || !toolId) return;
const isPinned = isToolPinned(toolId);
const layout = loadShortcutLayout();
const isFull = layout.length >= MAX_PINNED_SHORTCUTS;
const items = [];
if (isPinned) {
items.push({
label: t('shortcutBarUnpin'),
action: () => {
unpinTool(toolId);
onLayoutChange?.();
}
});
} else {
items.push({
label: isFull ? `${t('shortcutBarPin')} (${t('shortcutBarCapacityFull') ? '已满' : 'Full'})` : t('shortcutBarPin'),
disabled: isFull,
action: () => {
const res = pinTool(toolId);
if (!res.success && res.reason === 'capacity_full') {
showCapacityFullToast();
} else {
onLayoutChange?.();
}
}
});
}
const rect = targetEl.getBoundingClientRect();
showDropdownMenu(items, rect.left, rect.bottom + 4);
}
export function openShortcutButtonMenu(toolId, buttonEl, onLayoutChange) {
if (!buttonEl || !toolId) return;
const layout = loadShortcutLayout();
const idx = layout.indexOf(toolId);
if (idx === -1) return;
const items = [
{
label: t('shortcutBarMoveLeft'),
disabled: idx <= 0,
action: () => {
reorderShortcut(idx, idx - 1);
onLayoutChange?.();
}
},
{
label: t('shortcutBarMoveRight'),
disabled: idx >= layout.length - 1,
action: () => {
reorderShortcut(idx, idx + 1);
onLayoutChange?.();
}
},
{ separator: true },
{
label: t('shortcutBarUnpin'),
danger: true,
action: () => {
unpinTool(toolId);
onLayoutChange?.();
}
}
];
const rect = buttonEl.getBoundingClientRect();
showDropdownMenu(items, rect.left, rect.top - 4);
}
/**
* Cancel and clean up active drag operation
*/
function cancelCurrentDrag() {
if (autoOpenTimer) {
clearTimeout(autoOpenTimer);
autoOpenTimer = null;
}
if (activeDrag) {
if (activeDrag.ghostEl) {
activeDrag.ghostEl.remove();
}
if (activeDrag.indicatorEl) {
activeDrag.indicatorEl.remove();
}
if (activeDrag.dropzoneEl) {
activeDrag.dropzoneEl.classList.remove('is-drag-over');
const txt = activeDrag.dropzoneEl.querySelector('.anomalous-toolbox-dropzone-text');
if (txt) txt.textContent = t('shortcutDropHint');
}
if (activeDrag.originEl) {
activeDrag.originEl.style.opacity = '1';
}
}
setDraggingState(false);
activeDrag = null;
}
/**
* Binds pointer drag & drop to a shortcut bar button or toolbox card.
*/
export function bindDraggableTool(element, {
toolId,
source, // 'shortcut' | 'toolbox'
toolboxModal,
toolboxBtn,
sidebarActionsEl,
onLayoutChange,
onToolClick,
}) {
if (!element || !toolId) return;
let startX = 0;
let startY = 0;
let hasMoved = false;
const onPointerDown = (e) => {
// Only primary mouse button (or touch/pen)
if (e.button !== undefined && e.button !== 0 && e.pointerType === 'mouse') return;
// Ignore if clicking a "More" menu button inside card
if (e.target?.closest?.('.anomalous-tool-more-btn')) return;
startX = e.clientX;
startY = e.clientY;
hasMoved = false;
const spec = getToolDefinition(toolId);
if (!spec) return;
const onPointerMove = (moveEvent) => {
const dx = moveEvent.clientX - startX;
const dy = moveEvent.clientY - startY;
const dist = Math.hypot(dx, dy);
if (!hasMoved) {
if (dist > 6) {
hasMoved = true;
setDraggingState(true);
hideTooltipImmediately();
closeActiveMenu();
// Create ghost element
const ghost = document.createElement('div');
ghost.className = 'anomalous-shortcut-drag-ghost';
ghost.innerHTML = `
<div class="anomalous-ghost-icon">${spec.icon}</div>
<div class="anomalous-ghost-title">${t(spec.labelKey)}</div>
`;
ghost.style.position = 'fixed';
ghost.style.zIndex = '1000003';
ghost.style.pointerEvents = 'none';
ghost.style.transform = 'translate(-50%, -50%)';
ghost.style.display = 'flex';
ghost.style.alignItems = 'center';
ghost.style.gap = '6px';
ghost.style.padding = '6px 12px';
ghost.style.borderRadius = '8px';
ghost.style.background = 'rgba(28, 30, 38, 0.95)';
ghost.style.border = '1px solid rgba(255, 255, 255, 0.25)';
ghost.style.boxShadow = '0 12px 30px rgba(0,0,0,0.6)';
ghost.style.color = '#ffffff';
ghost.style.fontSize = '12px';
ghost.style.fontWeight = '600';
document.body.appendChild(ghost);
// Create drop indicator line for shortcut bar
const indicator = document.createElement('div');
indicator.className = 'anomalous-shortcut-drop-indicator';
indicator.style.display = 'none';
indicator.style.position = 'absolute';
indicator.style.width = '2px';
indicator.style.height = '24px';
indicator.style.background = '#ffffff';
indicator.style.boxShadow = '0 0 8px rgba(255, 255, 255, 0.8)';
indicator.style.borderRadius = '1px';
indicator.style.zIndex = '10';
indicator.style.pointerEvents = 'none';
if (sidebarActionsEl) {
sidebarActionsEl.appendChild(indicator);
}
const dropzone = toolboxModal ? toolboxModal.querySelector('.anomalous-toolbox-dropzone') : null;
activeDrag = {
isDragging: true,
toolId,
source,
originEl: element,
ghostEl: ghost,
indicatorEl: indicator,
dropzoneEl: dropzone,
targetIndex: -1,
dropAction: null, // 'pin' | 'unpin' | 'reorder' | null
};
element.style.opacity = '0.4';
}
}
if (activeDrag && activeDrag.isDragging) {
// Move ghost
activeDrag.ghostEl.style.left = `${moveEvent.clientX}px`;
activeDrag.ghostEl.style.top = `${moveEvent.clientY}px`;
const overEl = typeof document.elementFromPoint === 'function'
? document.elementFromPoint(moveEvent.clientX, moveEvent.clientY)
: null;
// Check if over closed toolbox button (auto-expand after 350ms)
if (toolboxBtn && toolboxBtn.contains(overEl)) {
if (toolboxModal && toolboxModal.style.display === 'none') {
if (!autoOpenTimer) {
autoOpenTimer = setTimeout(() => {
toolboxModal.style.display = 'flex';
}, 350);
}
}
} else {
if (autoOpenTimer) {
clearTimeout(autoOpenTimer);
autoOpenTimer = null;
}
}
// Check if over Toolbox or dropzone (for unpinning)
const isOverToolbox = toolboxModal && toolboxModal.style.display !== 'none' && (toolboxModal.contains(overEl) || (toolboxBtn && toolboxBtn.contains(overEl)));
if (isOverToolbox) {
if (activeDrag.dropzoneEl) {
activeDrag.dropzoneEl.classList.add('is-drag-over');
const txt = activeDrag.dropzoneEl.querySelector('.anomalous-toolbox-dropzone-text');
if (txt) txt.textContent = t('shortcutDropRelease');
}
if (activeDrag.indicatorEl) activeDrag.indicatorEl.style.display = 'none';
activeDrag.dropAction = 'unpin';
return;
} else {
if (activeDrag.dropzoneEl) {
activeDrag.dropzoneEl.classList.remove('is-drag-over');
const txt = activeDrag.dropzoneEl.querySelector('.anomalous-toolbox-dropzone-text');
if (txt) txt.textContent = t('shortcutDropHint');
}
}
// Check if over Shortcut Bar (for pinning / reordering)
const isOverShortcutBar = sidebarActionsEl && sidebarActionsEl.contains(overEl);
if (isOverShortcutBar) {
const buttons = Array.from(sidebarActionsEl.querySelectorAll('button[data-tool-id]'));
let foundIndex = buttons.length;
let indicatorX = 0;
const barRect = sidebarActionsEl.getBoundingClientRect();
for (let i = 0; i < buttons.length; i++) {
const bRect = buttons[i].getBoundingClientRect();
const bMidX = bRect.left + bRect.width / 2;
if (moveEvent.clientX < bMidX) {
foundIndex = i;
indicatorX = bRect.left - barRect.left - 2;
break;
}
}
if (foundIndex === buttons.length && buttons.length > 0) {
const lastRect = buttons[buttons.length - 1].getBoundingClientRect();
indicatorX = lastRect.right - barRect.left + 2;
}
activeDrag.targetIndex = foundIndex;
activeDrag.dropAction = 'pin';
if (activeDrag.indicatorEl) {
activeDrag.indicatorEl.style.display = 'block';
activeDrag.indicatorEl.style.left = `${Math.round(indicatorX)}px`;
activeDrag.indicatorEl.style.top = '6px';
}
} else {
if (activeDrag.indicatorEl) {
activeDrag.indicatorEl.style.display = 'none';
}
activeDrag.dropAction = null;
}
}
};
const onPointerUp = (upEvent) => {
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', onPointerUp);
document.removeEventListener('pointercancel', onPointerCancel);
if (autoOpenTimer) {
clearTimeout(autoOpenTimer);
autoOpenTimer = null;
}
if (!hasMoved) {
// Ordinary click!
if (onToolClick) {
onToolClick(upEvent);
}
return;
}
// Drag finished: handle drop action
if (activeDrag && activeDrag.isDragging) {
upEvent.preventDefault();
upEvent.stopPropagation();
const { dropAction, targetIndex } = activeDrag;
if (dropAction === 'unpin') {
unpinTool(toolId);
onLayoutChange?.();
} else if (dropAction === 'pin') {
const res = pinTool(toolId, targetIndex);
if (!res.success && res.reason === 'capacity_full') {
showCapacityFullToast();
} else {
onLayoutChange?.();
}
}
}
cancelCurrentDrag();
};
const onPointerCancel = () => {
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', onPointerUp);
document.removeEventListener('pointercancel', onPointerCancel);
cancelCurrentDrag();
};
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp);
document.addEventListener('pointercancel', onPointerCancel);
};
element.addEventListener('pointerdown', onPointerDown);
// Also support right-click context menu
element.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
if (source === 'shortcut') {
openShortcutButtonMenu(toolId, element, onLayoutChange);
} else {
openToolboxCardMenu(toolId, element, onLayoutChange);
}
});
}