code-graph-cli 3.0.3

Code intelligence engine for TypeScript/JavaScript/Rust/Python/Go — query the dependency graph instead of reading source files.
<script lang="ts">
  import type { NodeAttributes } from '../types';

  interface Props {
    x: number;
    y: number;
    visible: boolean;
    nodeKey: string;
    nodeAttributes: NodeAttributes | null;
    onAction: (action: string, nodeKey: string) => void;
  }

  let { x, y, visible, nodeKey, nodeAttributes, onAction }: Props = $props();

  interface MenuItem {
    label: string;
    action: string;
    icon: string;
  }

  const menuItems: MenuItem[] = [
    { label: 'Show references', action: 'references', icon: '→' },
    { label: 'Show impact', action: 'impact', icon: '↕' },
    { label: 'Focus neighborhood', action: 'focus', icon: '◎' },
    { label: 'Copy path', action: 'copy-path', icon: '⎘' },
    { label: 'Open in editor', action: 'open-editor', icon: '✎' },
  ];

  function handleAction(e: MouseEvent, action: string) {
    e.stopPropagation();
    onAction(action, nodeKey);
  }

  // Clamp position to keep menu in viewport
  const MENU_WIDTH = 200;
  const MENU_HEIGHT = 200;

  let clampedX = $derived(
    typeof window !== 'undefined' ? Math.min(x, window.innerWidth - MENU_WIDTH - 8) : x
  );
  let clampedY = $derived(
    typeof window !== 'undefined' ? Math.min(y, window.innerHeight - MENU_HEIGHT - 8) : y
  );
</script>

{#if visible}
  <!-- svelte-ignore a11y_no_static_element_interactions -->
  <div
    class="context-menu"
    style="left: {clampedX}px; top: {clampedY}px;"
    onclick={(e) => e.stopPropagation()}
    oncontextmenu={(e) => e.preventDefault()}
  >
    {#if nodeAttributes}
      <div class="menu-header">
        <span class="menu-node-name">{nodeAttributes.label || nodeKey}</span>
        <span class="menu-node-kind">{nodeAttributes.kind}</span>
      </div>
      <div class="menu-divider"></div>
    {/if}

    {#each menuItems as item}
      <button
        class="menu-item"
        onclick={(e) => handleAction(e, item.action)}
      >
        <span class="menu-icon">{item.icon}</span>
        <span class="menu-label">{item.label}</span>
      </button>
    {/each}
  </div>
{/if}

<style>
  .context-menu {
    position: fixed;
    z-index: 200;
    background: var(--color-bg-panel, #1a1a1c);
    border: 1px solid var(--color-border, #2a2a3e);
    border-radius: 6px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2);
    padding: 4px;
    min-width: 180px;
    user-select: none;
  }

  .menu-header {
    padding: 6px 10px;
    display: flex;
    flex-direction: column;
    gap: 2px;
  }

  .menu-node-name {
    font-size: 12px;
    font-weight: 600;
    color: var(--color-text-primary, #f5f5f5);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 160px;
  }

  .menu-node-kind {
    font-size: 10px;
    color: var(--color-text-muted, #6b7280);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }

  .menu-divider {
    height: 1px;
    background: var(--color-border, #2a2a3e);
    margin: 2px 0;
  }

  .menu-item {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 7px 10px;
    background: transparent;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    text-align: left;
    font-size: 13px;
    color: var(--color-text-primary, #f5f5f5);
    font-family: inherit;
    transition: background 100ms ease;
  }

  .menu-item:hover {
    background: rgba(255, 255, 255, 0.08);
  }

  .menu-icon {
    font-size: 12px;
    color: var(--color-text-muted, #6b7280);
    width: 14px;
    text-align: center;
    flex-shrink: 0;
  }

  .menu-label {
    flex: 1;
  }
</style>