atagen

malice

public
Star0
Watch0
0380cd8c765cdd7735d5c259c26abdeef712e742
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/ContextMenu.svelte

d8624c214c04atagen2026-10-24 08:24 UTC1
<script lang="ts" context="module">
d8624c214c04atagen2026-10-24 08:24 UTC2
  export interface MenuItem {
d8624c214c04atagen2026-10-24 08:24 UTC3
    label: string;
d8624c214c04atagen2026-10-24 08:24 UTC4
    action: () => void;
d8624c214c04atagen2026-10-24 08:24 UTC5
    danger?: boolean;
d8624c214c04atagen2026-10-24 08:24 UTC6
    disabled?: boolean;
d8624c214c04atagen2026-10-24 08:24 UTC7
  }
d8624c214c04atagen2026-10-24 08:24 UTC8
</script>
d8624c214c04atagen2026-10-24 08:24 UTC9
d8624c214c04atagen2026-10-24 08:24 UTC10
<script lang="ts">
d8624c214c04atagen2026-10-24 08:24 UTC11
  import { onMount, tick } from 'svelte';
d8624c214c04atagen2026-10-24 08:24 UTC12
d8624c214c04atagen2026-10-24 08:24 UTC13
  export let items: MenuItem[];
d8624c214c04atagen2026-10-24 08:24 UTC14
  export let x: number;
d8624c214c04atagen2026-10-24 08:24 UTC15
  export let y: number;
d8624c214c04atagen2026-10-24 08:24 UTC16
  export let onClose: () => void;
d8624c214c04atagen2026-10-24 08:24 UTC17
d8624c214c04atagen2026-10-24 08:24 UTC18
  const MARGIN = 8;
d8624c214c04atagen2026-10-24 08:24 UTC19
d8624c214c04atagen2026-10-24 08:24 UTC20
  let menu: HTMLDivElement;
d8624c214c04atagen2026-10-24 08:24 UTC21
  let left = x;
d8624c214c04atagen2026-10-24 08:24 UTC22
  let top = y;
d8624c214c04atagen2026-10-24 08:24 UTC23
d8624c214c04atagen2026-10-24 08:24 UTC24
  onMount(async () => {
d8624c214c04atagen2026-10-24 08:24 UTC25
    await tick();
d8624c214c04atagen2026-10-24 08:24 UTC26
    const rect = menu.getBoundingClientRect();
d8624c214c04atagen2026-10-24 08:24 UTC27
    left = Math.max(MARGIN, Math.min(x, window.innerWidth - rect.width - MARGIN));
d8624c214c04atagen2026-10-24 08:24 UTC28
    top = Math.max(MARGIN, Math.min(y, window.innerHeight - rect.height - MARGIN));
d8624c214c04atagen2026-10-24 08:24 UTC29
    menu.querySelector<HTMLButtonElement>('button:not(:disabled)')?.focus();
d8624c214c04atagen2026-10-24 08:24 UTC30
  });
d8624c214c04atagen2026-10-24 08:24 UTC31
d8624c214c04atagen2026-10-24 08:24 UTC32
  function pick(item: MenuItem): void {
d8624c214c04atagen2026-10-24 08:24 UTC33
    onClose();
d8624c214c04atagen2026-10-24 08:24 UTC34
    item.action();
d8624c214c04atagen2026-10-24 08:24 UTC35
  }
d8624c214c04atagen2026-10-24 08:24 UTC36
d8624c214c04atagen2026-10-24 08:24 UTC37
  function outside(ev: PointerEvent): void {
d8624c214c04atagen2026-10-24 08:24 UTC38
    if (!menu.contains(ev.target as Node)) onClose();
d8624c214c04atagen2026-10-24 08:24 UTC39
  }
d8624c214c04atagen2026-10-24 08:24 UTC40
d8624c214c04atagen2026-10-24 08:24 UTC41
  function key(ev: KeyboardEvent): void {
d8624c214c04atagen2026-10-24 08:24 UTC42
    if (ev.key === 'Escape') {
d8624c214c04atagen2026-10-24 08:24 UTC43
      ev.stopPropagation();
d8624c214c04atagen2026-10-24 08:24 UTC44
      onClose();
d8624c214c04atagen2026-10-24 08:24 UTC45
    }
d8624c214c04atagen2026-10-24 08:24 UTC46
  }
d8624c214c04atagen2026-10-24 08:24 UTC47
</script>
d8624c214c04atagen2026-10-24 08:24 UTC48
d8624c214c04atagen2026-10-24 08:24 UTC49
<svelte:window on:pointerdown|capture={outside} on:keydown|capture={key} on:resize={onClose} on:blur={onClose} />
d8624c214c04atagen2026-10-24 08:24 UTC50
d8624c214c04atagen2026-10-24 08:24 UTC51
<div class="context-menu" role="menu" style={`left:${left}px;top:${top}px`} bind:this={menu}>
d8624c214c04atagen2026-10-24 08:24 UTC52
  {#each items as item}
d8624c214c04atagen2026-10-24 08:24 UTC53
    <button type="button" role="menuitem" class:danger={item.danger} disabled={item.disabled} on:click={() => pick(item)}>
d8624c214c04atagen2026-10-24 08:24 UTC54
      {item.label}
d8624c214c04atagen2026-10-24 08:24 UTC55
    </button>
d8624c214c04atagen2026-10-24 08:24 UTC56
  {/each}
d8624c214c04atagen2026-10-24 08:24 UTC57
</div>
d8624c214c04atagen2026-10-24 08:24 UTC58
d8624c214c04atagen2026-10-24 08:24 UTC59
<style>
d8624c214c04atagen2026-10-24 08:24 UTC60
  .context-menu {
d8624c214c04atagen2026-10-24 08:24 UTC61
    position: fixed;
d8624c214c04atagen2026-10-24 08:24 UTC62
    z-index: 90;
d8624c214c04atagen2026-10-24 08:24 UTC63
    min-width: 180px;
d8624c214c04atagen2026-10-24 08:24 UTC64
    display: flex;
d8624c214c04atagen2026-10-24 08:24 UTC65
    flex-direction: column;
d8624c214c04atagen2026-10-24 08:24 UTC66
    padding: 4px;
d8624c214c04atagen2026-10-24 08:24 UTC67
    background: var(--panel-2);
d8624c214c04atagen2026-10-24 08:24 UTC68
    border: 1px solid var(--border);
d8624c214c04atagen2026-10-24 08:24 UTC69
    border-radius: 8px;
d8624c214c04atagen2026-10-24 08:24 UTC70
    box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);
d8624c214c04atagen2026-10-24 08:24 UTC71
  }
d8624c214c04atagen2026-10-24 08:24 UTC72
  button {
d8624c214c04atagen2026-10-24 08:24 UTC73
    background: transparent;
d8624c214c04atagen2026-10-24 08:24 UTC74
    border: none;
d8624c214c04atagen2026-10-24 08:24 UTC75
    border-radius: 6px;
d8624c214c04atagen2026-10-24 08:24 UTC76
    color: var(--text);
d8624c214c04atagen2026-10-24 08:24 UTC77
    font: inherit;
d8624c214c04atagen2026-10-24 08:24 UTC78
    font-size: 13.5px;
d8624c214c04atagen2026-10-24 08:24 UTC79
    text-align: left;
d8624c214c04atagen2026-10-24 08:24 UTC80
    padding: 7px 10px;
d8624c214c04atagen2026-10-24 08:24 UTC81
    cursor: pointer;
d8624c214c04atagen2026-10-24 08:24 UTC82
  }
d8624c214c04atagen2026-10-24 08:24 UTC83
  button:hover:not(:disabled), button:focus-visible { background: var(--hover); outline: none; }
d8624c214c04atagen2026-10-24 08:24 UTC84
  button:disabled { opacity: 0.5; cursor: default; }
d8624c214c04atagen2026-10-24 08:24 UTC85
  .danger { color: var(--danger); }
d8624c214c04atagen2026-10-24 08:24 UTC86
</style>