From 8a59f92d031963e23ecc84b75feecf43eb4dd146 Mon Sep 17 00:00:00 2001
From: "netop://ウィビ" <paul@webb.page>
Date: Fri, 24 Apr 2026 11:33:25 -0700
Subject: Initial commit: @eol/graphiql v0.3

Svelte 5 GraphiQL alternative for JSR. Covers:
- HTTP fetcher with injectable fetch; SSE/WS stubs
- Session store with tabs, auto-titling, persistence, rename
- Operation detection via graphql parse(); Toolbar picker
- CodeMirror 6 editor via cm6-graphql with theme prop
- Light theme preset (hand-rolled EditorView.theme)
- Doc explorer with breadcrumb nav and type guards
- History panel with 100-entry cap, favorite pinning
- Deno tests for operations, storage, and history eviction
---
 source/library/components/TabBar.svelte | 161 ++++++++++++++++++++++++++++++++
 1 file changed, 161 insertions(+)
 create mode 100644 source/library/components/TabBar.svelte

(limited to 'source/library/components/TabBar.svelte')

diff --git a/source/library/components/TabBar.svelte b/source/library/components/TabBar.svelte
new file mode 100644
index 0000000..d87449d
--- /dev/null
+++ b/source/library/components/TabBar.svelte
@@ -0,0 +1,161 @@
+<script lang="ts">
+  import { tick } from "svelte";
+  import type { Tab } from "../state/session.svelte.ts";
+
+  type Props = {
+    activeId: string;
+    onAdd: () => void;
+    onClose: (id: string) => void;
+    onRename: (id: string, title: string) => void;
+    onSelect: (id: string) => void;
+    tabs: Tab[];
+  };
+
+  let { activeId, onAdd, onClose, onRename, onSelect, tabs }: Props = $props();
+
+  let editingId = $state<string | null>(null);
+  let draft = $state<string>("");
+  let inputEl = $state<HTMLInputElement | null>(null);
+
+  async function startEditing(tab: Tab) {
+    editingId = tab.id;
+    draft = tab.title;
+    await tick();
+    inputEl?.select();
+  }
+
+  function commit() {
+    if (editingId === null) return;
+    onRename(editingId, draft);
+    editingId = null;
+    draft = "";
+  }
+
+  function cancel() {
+    editingId = null;
+    draft = "";
+  }
+
+  function onKeydown(event: KeyboardEvent) {
+    if (event.key === "Enter") {
+      event.preventDefault();
+      commit();
+    } else if (event.key === "Escape") {
+      event.preventDefault();
+      cancel();
+    }
+  }
+
+  function handleClose(event: MouseEvent, id: string) {
+    event.stopPropagation();
+    onClose(id);
+  }
+</script>
+
+<style lang="scss">
+  .tabbar {
+    align-items: stretch;
+    background: var(--graphiql-panel, #252526);
+    border-bottom: 1px solid var(--graphiql-border, #333);
+    display: flex;
+    font-size: 0.8125rem;
+    min-height: 2rem;
+    overflow-x: auto;
+  }
+
+  .tab {
+    align-items: center;
+    background: transparent;
+    border: none;
+    border-right: 1px solid var(--graphiql-border, #333);
+    color: var(--graphiql-muted, #858585);
+    cursor: pointer;
+    display: flex;
+    gap: 0.5rem;
+    padding: 0 0.75rem;
+
+    &.active {
+      background: var(--graphiql-bg, #1e1e1e);
+      color: var(--graphiql-fg, #d4d4d4);
+    }
+
+    &:hover:not(.active) {
+      color: var(--graphiql-fg, #d4d4d4);
+    }
+  }
+
+  .title {
+    white-space: nowrap;
+  }
+
+  .edit {
+    background: var(--graphiql-bg, #1e1e1e);
+    border: 1px solid var(--graphiql-accent, #0e639c);
+    border-radius: 2px;
+    color: var(--graphiql-fg, #d4d4d4);
+    font-family: inherit;
+    font-size: inherit;
+    min-width: 6rem;
+    outline: none;
+    padding: 0.125rem 0.25rem;
+  }
+
+  .close {
+    background: none;
+    border: none;
+    color: inherit;
+    cursor: pointer;
+    font-size: 1rem;
+    line-height: 1;
+    opacity: 0.6;
+    padding: 0;
+
+    &:hover {
+      opacity: 1;
+    }
+  }
+
+  .add {
+    background: none;
+    border: none;
+    color: var(--graphiql-muted, #858585);
+    cursor: pointer;
+    font-size: 1rem;
+    padding: 0 0.75rem;
+
+    &:hover {
+      color: var(--graphiql-fg, #d4d4d4);
+    }
+  }
+</style>
+
+<div class="tabbar">
+  {#each tabs as tab (tab.id)}
+    <button
+      class="tab"
+      class:active={tab.id === activeId}
+      ondblclick={() => startEditing(tab)}
+      onclick={() => onSelect(tab.id)}
+    >
+      {#if editingId === tab.id}
+        <input
+          bind:this={inputEl}
+          bind:value={draft}
+          class="edit"
+          onblur={commit}
+          onclick={(e) => e.stopPropagation()}
+          onkeydown={onKeydown}
+          type="text"
+        />
+      {:else}
+        <span class="title">{tab.title}</span>
+      {/if}
+      <button
+        aria-label="Close tab"
+        class="close"
+        onclick={(e) => handleClose(e, tab.id)}
+      >×</button>
+    </button>
+  {/each}
+  <button aria-label="New tab" class="add" onclick={onAdd}>+</button>
+</div>
-- 
cgit v1.2.3

