wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<script lang="ts">
  import type { GitHubGist } from "../../api/types.js";
  import { contextMenu as placeMenu } from "../../utils/menu.js";
  import { copyToClipboard } from "../../utils/clipboard.js";

  interface Props {
    gists: GitHubGist[];
    loading: boolean;
    selectedId: string | null;
    onselect: (id: string) => void;
  }

  let {
    gists,
    loading,
    selectedId,
    onselect,
  }: Props = $props();

  let contextMenu: { x: number; y: number; gist: GitHubGist } | null = $state(null);

  function portal(node: HTMLElement) {
    document.body.appendChild(node);
    return { destroy() { node.remove(); } };
  }

  function handleGistContextMenu(e: MouseEvent, gist: GitHubGist) {
    e.preventDefault();
    contextMenu = { x: e.clientX, y: e.clientY, gist };
  }

  function closeContextMenu() { contextMenu = null; }

  function gistOpenInBrowser() {
    if (!contextMenu) return;
    window.open(`https://gist.github.com/${contextMenu.gist.id}`, "_blank");
    closeContextMenu();
  }

  function gistCopyLink() {
    if (!contextMenu) return;
    void copyToClipboard(`https://gist.github.com/${contextMenu.gist.id}`).catch(() => {});
    closeContextMenu();
  }

  function gistCopyId() {
    if (!contextMenu) return;
    void copyToClipboard(contextMenu.gist.id).catch(() => {});
    closeContextMenu();
  }

  $effect(() => {
    if (!contextMenu) return;
    function handler() { contextMenu = null; }
    const id = setTimeout(() => {
      document.addEventListener("click", handler, { once: true });
      document.addEventListener("contextmenu", handler, { once: true });
    }, 0);
    return () => {
      clearTimeout(id);
      document.removeEventListener("click", handler);
      document.removeEventListener("contextmenu", handler);
    };
  });

  $effect(() => {
    if (!contextMenu) return;
    function handler(e: KeyboardEvent) { if (e.key === "Escape") contextMenu = null; }
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  });

  function fileCount(gist: GitHubGist): number {
    return Object.keys(gist.files ?? {}).length;
  }
  function ownerName(gist: GitHubGist): string {
    return gist.owner?.login ?? "unknown";
  }
</script>

<div class="gist-list">
  {#if loading && gists.length === 0}
    <div class="skel-list">
      {#each { length: 5 } as _, i}
        <div class="skel-gist-item">
          <div class="skel" style="width: {50 + ((i * 23) % 40)}%; height: var(--wb-control-size-11);"></div>
          <div class="skel" style="width: {30 + ((i * 19) % 50)}%; height: var(--wb-control-size-8); margin-top: var(--wb-spacing-6);"></div>
        </div>
      {/each}
    </div>
  {:else if gists.length === 0}
    <div class="gist-list-empty">No published gists yet.</div>
  {:else}
    <div class="gist-list-items">
      {#each gists as gist}
        <button
          class="gist-item"
          class:active={selectedId === gist.id}
          onclick={() => onselect(gist.id)}
          oncontextmenu={(e) => handleGistContextMenu(e, gist)}
        >
          <div class="gist-item-main">
            <div class="gist-item-title">
              {gist.description?.trim() || "Untitled gist"}
            </div>
            <div class="gist-item-meta">
              <span>{ownerName(gist)}</span>
              <span>{fileCount(gist)} files</span>
              <span>{gist.public ? "Public" : "Secret"}</span>
            </div>
          </div>
          <time class="gist-item-time" datetime={gist.updated_at}>
            {new Date(gist.updated_at).toLocaleString()}
          </time>
        </button>
      {/each}
    </div>
  {/if}
</div>

{#if contextMenu}
  <div
    class="context-menu"
    use:placeMenu={contextMenu}
  >
    <button class="context-menu-item" onclick={gistOpenInBrowser}>Open in Browser</button>
    <div class="context-menu-separator"></div>
    <button class="context-menu-item" onclick={gistCopyLink}>Copy Link</button>
    <button class="context-menu-item" onclick={gistCopyId}>Copy Gist ID</button>
  </div>
{/if}

<style>
  .gist-list {
    height: 100%;
    display: flex;
    flex-direction: column;
    background: var(--vscode-sideBar-background);
  }
  .gist-list-items {
    overflow-y: auto;
  }
  .gist-item {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: var(--wb-spacing-6);
    text-align: left;
    padding: var(--wb-spacing-14) var(--wb-spacing-16);
    border-bottom: 1px solid var(--vscode-panel-border, var(--vscode-panel-border));
    background: transparent;
    cursor: pointer;
  }
  .gist-item:hover,
  .gist-item.active {
    background: color-mix(in srgb, var(--vscode-focusBorder) 8%, transparent);
  }
  .gist-item-title {
    font-size: var(--wb-font-size-13);
    font-weight: 600;
    color: var(--vscode-editor-foreground);
  }
  .gist-item-meta {
    display: flex;
    gap: var(--wb-spacing-10);
    flex-wrap: wrap;
    font-size: var(--wb-font-size-11);
    color: var(--vscode-disabledForeground);
  }
  .gist-item-time {
    font-size: var(--wb-font-size-11);
    color: var(--vscode-disabledForeground);
  }
  .gist-list-empty {
    padding: var(--wb-spacing-24) var(--wb-spacing-16);
    color: var(--vscode-disabledForeground);
    font-size: var(--wb-font-size-12);
  }
  .skel-list {
    display: flex;
    flex-direction: column;
  }
  .skel-gist-item {
    padding: var(--wb-spacing-14) var(--wb-spacing-16);
    border-bottom: 1px solid var(--vscode-panel-border);
  }

</style>