diff --git a/docs/plans/code-structure-refactor.md b/docs/plans/code-structure-refactor.md index 4d8fc29..8011c92 100644 --- a/docs/plans/code-structure-refactor.md +++ b/docs/plans/code-structure-refactor.md @@ -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`,复用生命周期工具,不改变尺寸偏好。 diff --git a/docs/plans/customizable-tool-shortcuts.md b/docs/plans/customizable-tool-shortcuts.md index 159aa1d..a222066 100644 --- a/docs/plans/customizable-tool-shortcuts.md +++ b/docs/plans/customizable-tool-shortcuts.md @@ -1,6 +1,6 @@ # 工具箱与快捷栏自定义:交给 Gemini 的实施计划 -状态:待实施的产品与工程计划,不代表当前版本已经具备以下功能。 +状态:已放弃。当前是固定快捷栏(见 `ui_toolbox.js`);按本计划写的 `shortcut_layout.js`、`ui_shortcut_organizer.js` 从未接入,已删除。只作历史参考,不要照此实施。 基线:`77ae7ce88`;开工时以实际 Git 差异和现有代码为准,不回退后续工作。 ## 目标与范围 diff --git a/web/modules/locales.js b/web/modules/locales.js index b907b47..cbad8ee 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -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", diff --git a/web/modules/shortcut_layout.js b/web/modules/shortcut_layout.js deleted file mode 100644 index 4207102..0000000 --- a/web/modules/shortcut_layout.js +++ /dev/null @@ -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); -} diff --git a/web/modules/ui_shortcut_organizer.js b/web/modules/ui_shortcut_organizer.js deleted file mode 100644 index 49734c7..0000000 --- a/web/modules/ui_shortcut_organizer.js +++ /dev/null @@ -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 = ` -
${spec.icon}
-
${t(spec.labelKey)}
- `; - 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); - } - }); -}