From 510fd8cbe53abb39cba2c7cbaaefcf2783dc0066 Mon Sep 17 00:00:00 2001 From: "netop://ウィビ" Date: Fri, 24 Apr 2026 16:37:33 -0700 Subject: Implement v0.6-1.0: shortcuts, format, export/import, splitter, timing, APQ - v0.6: matchShortcut + format(); Cmd+Shift+Enter/W/F + Cmd+Alt+arrows - v0.7: SessionStore.exportAll/importTabs with version-1 validator - v0.8: Splitter component + four resize handles persisted under layout.* - v0.10: createApqFetcher (HTTP-only) wrapping shared http-body helpers - Drop .svelte re-exports from index.ts for multi-entry JSR/npm publishing --- source/library/GraphiQL.svelte | 307 +++++++++++++++++++++++++++++++++++------ 1 file changed, 263 insertions(+), 44 deletions(-) (limited to 'source/library/GraphiQL.svelte') diff --git a/source/library/GraphiQL.svelte b/source/library/GraphiQL.svelte index 329dd6f..4250fd3 100644 --- a/source/library/GraphiQL.svelte +++ b/source/library/GraphiQL.svelte @@ -10,14 +10,17 @@ import HeadersEditor from "./components/HeadersEditor.svelte"; import HistoryPanel from "./components/HistoryPanel.svelte"; import ResultViewer from "./components/ResultViewer.svelte"; + import Splitter from "./components/Splitter.svelte"; import TabBar from "./components/TabBar.svelte"; import Toolbar from "./components/Toolbar.svelte"; import type { Extension } from "@codemirror/state"; import type { Fetcher } from "./fetcher/types.ts"; import { HistoryStore } from "./state/history.svelte.ts"; + import { matchShortcut } from "./state/keyboard.ts"; import { SchemaStore } from "./state/schema.svelte.ts"; import { SessionStore } from "./state/session.svelte.ts"; import type { SubscriptionMode, Tab } from "./state/session.svelte.ts"; + import { validateSessionExport } from "./state/session-io.ts"; import { createLocalStorage, createMemoryStorage } from "./state/storage.ts"; import type { Storage } from "./state/storage.ts"; @@ -59,10 +62,28 @@ session.updateQuery(session.active.id, initialQuery); let bottomPane = $state<"variables" | "headers">("variables"); + let bottomHeight = $state(resolvedStorage.get("layout.bottomHeight") ?? 35); + let centerEl = $state(null); let docsOpen = $state(resolvedStorage.get("docExplorer") ?? false); + let docsWidth = $state(resolvedStorage.get("layout.docsWidth") ?? 320); + let historyNotice = $state(null); let historyOpen = $state(resolvedStorage.get("historyPanel") ?? false); + let historyWidth = $state(resolvedStorage.get("layout.historyWidth") ?? 260); + let leftEl = $state(null); + let leftWidth = $state(resolvedStorage.get("layout.leftWidth") ?? 50); let running = $state(false); + let dragStartHistoryWidth = 0; + let dragStartDocsWidth = 0; + let dragStartLeftWidth = 0; + let dragStartLeftWidthPx = 0; + let dragStartBottomHeight = 0; + let dragStartBottomHeightPx = 0; + + function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); + } + $effect(() => { void session.tabs; void session.activeId; @@ -92,6 +113,22 @@ resolvedStorage.set("historyPanel", historyOpen); }); + $effect(() => { + resolvedStorage.set("layout.bottomHeight", bottomHeight); + }); + + $effect(() => { + resolvedStorage.set("layout.docsWidth", docsWidth); + }); + + $effect(() => { + resolvedStorage.set("layout.historyWidth", historyWidth); + }); + + $effect(() => { + resolvedStorage.set("layout.leftWidth", leftWidth); + }); + onMount(() => { schema.introspect(fetcher); }); @@ -149,6 +186,61 @@ session.overwriteActive(seed); } + function onExportSession() { + if (typeof globalThis.URL === "undefined") + return; + + if (typeof globalThis.document === "undefined") + return; + + const data = session.exportAll(); + const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = globalThis.document.createElement("a"); + a.download = `graphiql-session-${data.exportedAt}.json`; + a.href = url; + a.click(); + URL.revokeObjectURL(url); + } + + async function onImportSession(file: File) { + let text: string; + + try { + text = await file.text(); + } catch (err) { + historyNotice = `Import failed: ${String(err)}`; + return; + } + + let parsed: unknown; + + try { + parsed = JSON.parse(text); + } catch (err) { + historyNotice = `Import failed: invalid JSON (${String(err)})`; + return; + } + + const valid = validateSessionExport(parsed); + + if ("error" in valid) { + historyNotice = `Import failed: ${valid.error}`; + return; + } + + const result = session.importTabs(valid, { mode: "append" }); + const parts = [`Imported ${result.added} tab${result.added === 1 ? "" : "s"}`]; + + if (result.skipped > 0) + parts.push(`skipped ${result.skipped}`); + + if (result.errors.length > 0) + parts.push(`${result.errors.length} error${result.errors.length === 1 ? "" : "s"}`); + + historyNotice = parts.join(", "); + } + function onQueryChange(value: string) { if (session.active) session.updateQuery(session.active.id, value); @@ -165,17 +257,96 @@ } function onKeydown(event: KeyboardEvent) { - const meta = event.metaKey || event.ctrlKey; + const action = matchShortcut(event); - if (meta && event.key === "Enter") { - event.preventDefault(); - run(); + if (!action) + return; + + event.preventDefault(); + + switch (action.type) { + case "closeTab": + session.closeTab(session.activeId); + break; + case "format": + session.formatActive(); + break; + case "newTab": + session.addTab(); + break; + case "nextTab": + session.nextTab(); + break; + case "prevTab": + session.prevTab(); + break; + case "run": + run(); + break; } } function onBeforeUnload() { session.persist(); } + + function onHistoryDragStart() { + dragStartHistoryWidth = historyWidth; + } + + function onHistoryDrag(dx: number) { + historyWidth = clamp(dragStartHistoryWidth + dx, 200, 500); + } + + function onHistoryKeyAdjust(delta: number) { + historyWidth = clamp(historyWidth + delta, 200, 500); + } + + function onDocsDragStart() { + dragStartDocsWidth = docsWidth; + } + + function onDocsDrag(dx: number) { + docsWidth = clamp(dragStartDocsWidth - dx, 240, 600); + } + + function onDocsKeyAdjust(delta: number) { + docsWidth = clamp(docsWidth - delta, 240, 600); + } + + function onLeftDragStart() { + dragStartLeftWidth = leftWidth; + dragStartLeftWidthPx = centerEl?.getBoundingClientRect().width ?? 0; + } + + function onLeftDrag(dx: number) { + if (dragStartLeftWidthPx === 0) + return; + + const percentDelta = (dx / dragStartLeftWidthPx) * 100; + leftWidth = clamp(dragStartLeftWidth + percentDelta, 20, 80); + } + + function onLeftKeyAdjust(delta: number) { + leftWidth = clamp(leftWidth + delta, 20, 80); + } + + function onBottomDragStart() { + dragStartBottomHeight = bottomHeight; + dragStartBottomHeightPx = leftEl?.getBoundingClientRect().height ?? 0; + } + + function onBottomDrag(_dx: number, dy: number) { + if (dragStartBottomHeightPx === 0) + return; + + const percentDelta = (dy / dragStartBottomHeightPx) * 100; + bottomHeight = clamp(dragStartBottomHeight - percentDelta, 15, 70); + } + + function onBottomKeyAdjust(delta: number) { + bottomHeight = clamp(bottomHeight - delta, 15, 70); + }