aboutsummaryrefslogtreecommitdiff
path: root/source/library/GraphiQL.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'source/library/GraphiQL.svelte')
-rw-r--r--source/library/GraphiQL.svelte392
1 files changed, 243 insertions, 149 deletions
diff --git a/source/library/GraphiQL.svelte b/source/library/GraphiQL.svelte
index 0f1e0fd..3ffc372 100644
--- a/source/library/GraphiQL.svelte
+++ b/source/library/GraphiQL.svelte
@@ -1,28 +1,28 @@
<script lang="ts">
/*** IMPORT ------------------------------------------- ***/
-
+ import "@inc/uchu/css";
import { onMount, type Snippet } from "svelte";
+ import type { Extension } from "@codemirror/state";
/*** UTILITY ------------------------------------------ ***/
-
import DocExplorer from "./components/DocExplorer.svelte";
import Editor from "./components/Editor.svelte";
- 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 { createLocalStorage, createMemoryStorage } from "./state/storage.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 { Fetcher } from "./fetcher/types.ts";
import type { Storage } from "./state/storage.ts";
+ import type { SubscriptionMode, Tab } from "./state/session.svelte.ts";
type Props = {
fetcher: Fetcher;
@@ -36,6 +36,9 @@
toolbarExtras?: Snippet;
};
+ const MINIMUM_HISTORY_WIDTH = 300;
+ const PERSIST_DEBOUNCE_MS = 300;
+
let {
fetcher,
initialQuery = "",
@@ -55,38 +58,59 @@
createLocalStorage(namespace) :
createMemoryStorage());
- const PERSIST_DEBOUNCE_MS = 300;
const history = new HistoryStore(resolvedStorage);
const schema = new SchemaStore();
const session = new SessionStore(resolvedStorage);
// svelte-ignore state_referenced_locally
- if (initialQuery && session.active && session.active.query === "")
+ if (initialQuery && session.active && session.active.query === "") {
// svelte-ignore state_referenced_locally
session.updateQuery(session.active.id, initialQuery);
+ }
let bottomPane = $state<"variables" | "headers">("variables");
let bottomHeight = $state(resolvedStorage.get<number>("layout.bottomHeight") ?? 35);
let centerEl = $state<HTMLDivElement | null>(null);
let docsOpen = $state(resolvedStorage.get<boolean>("docExplorer") ?? false);
let docsWidth = $state(resolvedStorage.get<number>("layout.docsWidth") ?? 320);
+ let dragStartBottomHeight = 0;
+ let dragStartBottomHeightPx = 0;
+ let dragStartDocsWidth = 0;
+ let dragStartHistoryWidth = 0;
+ let dragStartLeftWidth = 0;
+ let dragStartLeftWidthPx = 0;
let historyNotice = $state<string | null>(null);
let historyOpen = $state(resolvedStorage.get<boolean>("historyPanel") ?? false);
- let historyWidth = $state(resolvedStorage.get<number>("layout.historyWidth") ?? 260);
+ let historyWidth = $state(resolvedStorage.get<number>("layout.historyWidth") ?? MINIMUM_HISTORY_WIDTH);
let leftEl = $state<HTMLDivElement | null>(null);
let leftWidth = $state(resolvedStorage.get<number>("layout.leftWidth") ?? 50);
+ let runAbort: AbortController | null = null;
let running = $state(false);
+ let windowWidth = $state(0);
+ let windowHeight = $state(0);
+ let windowResizeTimeout: ReturnType<typeof setTimeout>;
+
+
- 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(() => {
+ const updateSize = () => {
+ clearTimeout(windowResizeTimeout);
+
+ windowResizeTimeout = setTimeout(() => {
+ windowWidth = globalThis.innerWidth;
+ windowHeight = globalThis.innerHeight;
+ }, 100);
+ };
+
+ updateSize();
+ globalThis.addEventListener("resize", updateSize);
+
+ return () => {
+ globalThis.removeEventListener("resize", updateSize);
+ };
+ });
$effect(() => {
void session.tabs;
@@ -137,7 +161,10 @@
schema.introspect(fetcher);
});
- let runAbort: AbortController | null = null;
+ /*** HELPER ------------------------------------------- ***/
+ function clamp(value: number, min: number, max: number): number {
+ return Math.min(Math.max(value, min), max);
+ }
async function run() {
if (running) {
@@ -190,6 +217,39 @@
session.overwriteActive(seed);
}
+ function onBeforeUnload() {
+ session.persist();
+ }
+
+ function onBottomDrag(_dx: number, dy: number) {
+ if (dragStartBottomHeightPx === 0)
+ return;
+
+ const percentDelta = (dy / dragStartBottomHeightPx) * 100;
+ bottomHeight = clamp(dragStartBottomHeight - percentDelta, 15, 70);
+ }
+
+ function onBottomDragStart() {
+ dragStartBottomHeight = bottomHeight;
+ dragStartBottomHeightPx = leftEl?.getBoundingClientRect().height ?? 0;
+ }
+
+ function onBottomKeyAdjust(delta: number) {
+ bottomHeight = clamp(bottomHeight - delta, 15, 70);
+ }
+
+ function onDocsDrag(dx: number) {
+ docsWidth = clamp(dragStartDocsWidth - dx, 240, 600);
+ }
+
+ function onDocsDragStart() {
+ dragStartDocsWidth = docsWidth;
+ }
+
+ function onDocsKeyAdjust(delta: number) {
+ docsWidth = clamp(docsWidth - delta, 240, 600);
+ }
+
function onExportSession() {
if (typeof globalThis.URL === "undefined")
return;
@@ -201,12 +261,31 @@
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);
}
+ function onHeadersChange(value: string) {
+ if (session.active)
+ session.active.headers = value;
+ }
+
+ function onHistoryDrag(dx: number) {
+ historyWidth = clamp(dragStartHistoryWidth + dx, MINIMUM_HISTORY_WIDTH, 500);
+ }
+
+ function onHistoryDragStart() {
+ dragStartHistoryWidth = historyWidth;
+ }
+
+ function onHistoryKeyAdjust(delta: number) {
+ historyWidth = clamp(historyWidth + delta, MINIMUM_HISTORY_WIDTH, 500);
+ }
+
async function onImportSession(file: File) {
let text: string;
@@ -245,21 +324,6 @@
historyNotice = parts.join(", ");
}
- function onQueryChange(value: string) {
- if (session.active)
- session.updateQuery(session.active.id, value);
- }
-
- function onVariablesChange(value: string) {
- if (session.active)
- session.active.variables = value;
- }
-
- function onHeadersChange(value: string) {
- if (session.active)
- session.active.headers = value;
- }
-
function onKeydown(event: KeyboardEvent) {
const action = matchShortcut(event);
@@ -269,60 +333,38 @@
event.preventDefault();
switch (action.type) {
- case "closeTab":
+ case "closeTab": {
session.closeTab(session.activeId);
break;
- case "format":
+ }
+
+ case "format": {
session.formatActive();
break;
- case "newTab":
+ }
+
+ case "newTab": {
session.addTab();
break;
- case "nextTab":
+ }
+
+ case "nextTab": {
session.nextTab();
break;
- case "prevTab":
+ }
+
+ case "prevTab": {
session.prevTab();
break;
- case "run":
+ }
+
+ 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;
@@ -331,25 +373,23 @@
leftWidth = clamp(dragStartLeftWidth + percentDelta, 20, 80);
}
- function onLeftKeyAdjust(delta: number) {
- leftWidth = clamp(leftWidth + delta, 20, 80);
+ function onLeftDragStart() {
+ dragStartLeftWidth = leftWidth;
+ dragStartLeftWidthPx = centerEl?.getBoundingClientRect().width ?? 0;
}
- function onBottomDragStart() {
- dragStartBottomHeight = bottomHeight;
- dragStartBottomHeightPx = leftEl?.getBoundingClientRect().height ?? 0;
+ function onLeftKeyAdjust(delta: number) {
+ leftWidth = clamp(leftWidth + delta, 20, 80);
}
- function onBottomDrag(_dx: number, dy: number) {
- if (dragStartBottomHeightPx === 0)
- return;
-
- const percentDelta = (dy / dragStartBottomHeightPx) * 100;
- bottomHeight = clamp(dragStartBottomHeight - percentDelta, 15, 70);
+ function onQueryChange(value: string) {
+ if (session.active)
+ session.updateQuery(session.active.id, value);
}
- function onBottomKeyAdjust(delta: number) {
- bottomHeight = clamp(bottomHeight - delta, 15, 70);
+ function onVariablesChange(value: string) {
+ if (session.active)
+ session.active.variables = value;
}
</script>
@@ -357,11 +397,22 @@
.graphiql {
width: 100%; height: 100%;
- background-color: var(--graphiql-bg, #1e1e1e);
- color: var(--graphiql-fg, #d4d4d4);
- display: grid;
- font-family: var(--graphiql-font, ui-monospace, SFMono-Regular, monospace);
- grid-template-rows: auto auto 1fr;
+ background-color: var(--uchu-yang);
+ color: var(--uchu-yin-9);
+ font-family: "Berkeley Mono", ui-monospace, SFMono-Regular, monospace;
+ font-size: 16px;
+ overflow: hidden;
+ overscroll-behavior: none;
+
+ @media (min-width: 1025px) {
+ display: grid;
+ grid-template-rows: auto auto 1fr;
+ }
+
+ @media (max-width: 1024px) {
+ display: flex;
+ flex-direction: column;
+ }
}
.panes {
@@ -369,6 +420,10 @@
grid-template-columns: 1fr;
min-height: 0;
+ @media (max-width: 1024px) {
+ height: stretch;
+ }
+
&.history-open {
grid-template-columns: var(--graphiql-history-width) 6px 1fr;
}
@@ -382,61 +437,91 @@
}
}
- .center {
- display: grid;
- grid-template-columns: var(--graphiql-left-width) 6px calc(100% - var(--graphiql-left-width) - 6px);
- min-height: 0;
- min-width: 0;
- }
-
+ .center,
.left {
- display: grid;
- grid-template-rows: 1fr auto 6px var(--graphiql-bottom-height);
+ margin-right: -6px;
min-height: 0;
min-width: 0;
}
- .query {
- min-height: 0;
- }
+ .center {
+ @media (min-width: 1025px) {
+ display: grid;
+ grid-template-columns: var(--graphiql-left-width) 6px calc(100% - var(--graphiql-left-width) - 6px);
+ }
- .switcher {
- background-color: var(--graphiql-panel, #252526);
- border-top: 1px solid var(--graphiql-border, #333);
- display: flex;
- }
+ @media (max-width: 1024px) {
+ display: flex;
+ flex-direction: column;
+ }
- .switch {
- background: none;
- border: none;
- cursor: pointer;
- font-size: 0.75rem;
- letter-spacing: 0.05em;
- padding: 0.375rem 0.75rem;
- text-transform: uppercase;
+ .query {
+ flex: 2;
+ margin-bottom: -6px;
+ min-height: 0;
+ }
- &:not(.active) {
- color: var(--graphiql-muted, #858585);
+ .bottom {
+ flex: 1;
+ min-height: 0;
}
- &.active {
- color: var(--graphiql-fg, #d4d4d4);
+ .left {
+ display: flex;
+ flex-direction: column;
+ overflow-x: hidden;
+
+ @media (max-width: 1024px) {
+ flex: 2;
+ }
}
- }
- .bottom {
- min-height: 0;
+ .right {
+ min-height: 0;
+
+ @media (max-width: 1024px) {
+ border-top: 2px solid var(--uchu-gray-3);
+ flex: 1;
+ }
+ }
}
- .right {
- min-height: 0;
+ .switcher {
+ background-color: var(--uchu-gray-2);
+ display: flex;
+ flex-direction: row;
+
+ .switch {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-family: "Berkeley Mono", ui-monospace, SFMono-Regular, monospace;
+ font-size: 0.75rem;
+ font-weight: 600;
+ letter-spacing: 0.05rem;
+ padding: 0.375rem 0.75rem;
+ text-transform: uppercase;
+
+ &:not(.active) {
+ color: var(--uchu-yin-5);
+ }
+
+ &.active {
+ background-color: var(--uchu-gray-3);
+ }
+ }
}
.status {
- background-color: var(--graphiql-panel, #252526);
- border-top: 1px solid var(--graphiql-border, #333);
+ background-color: var(--uchu-yellow-2);
+ border-top: 1px solid var(--uchu-yellow-3);
+ color: var(--uchu-yellow-9);
font-size: 0.75rem;
padding: 0.25rem 0.75rem;
+
+ span {
+ text-transform: uppercase;
+ }
}
</style>
@@ -473,8 +558,7 @@
class="panes"
class:docs-open={docsOpen && schema.schema}
class:history-open={historyOpen}
- style="--graphiql-history-width: {historyWidth}px; --graphiql-docs-width: {docsWidth}px;"
- >
+ style="--graphiql-history-width: {historyWidth}px; --graphiql-docs-width: {docsWidth}px;">
{#if historyOpen}
<HistoryPanel
entries={history.entries}
@@ -492,11 +576,11 @@
onKeyAdjust={onHistoryKeyAdjust}
orientation="horizontal"/>
{/if}
+
<div
bind:this={centerEl}
class="center"
- style="--graphiql-left-width: {leftWidth}%;"
- >
+ style="--graphiql-left-width: {leftWidth}%;">
<div bind:this={leftEl} class="left" style="--graphiql-bottom-height: {bottomHeight}%;">
<div class="query">
<Editor
@@ -506,22 +590,25 @@
{theme}
value={session.active?.query ?? ""}/>
</div>
- <div class="switcher">
- <button
- class="switch"
- class:active={bottomPane === "variables"}
- onclick={() => (bottomPane = "variables")}>Variables</button>
- <button
- class="switch"
- class:active={bottomPane === "headers"}
- onclick={() => (bottomPane = "headers")}>Headers</button>
- </div>
+
<Splitter
onDrag={onBottomDrag}
onDragStart={onBottomDragStart}
onKeyAdjust={onBottomKeyAdjust}
orientation="vertical"/>
+
<div class="bottom">
+ <div class="switcher">
+ <button
+ class="switch"
+ class:active={bottomPane === "variables"}
+ onclick={() => (bottomPane = "variables")}>Variables</button>
+ <button
+ class="switch"
+ class:active={bottomPane === "headers"}
+ onclick={() => (bottomPane = "headers")}>Headers</button>
+ </div>
+
{#if bottomPane === "variables"}
<Editor
language="json"
@@ -529,18 +616,23 @@
{theme}
value={session.active?.variables ?? "{}"}/>
{:else}
- <HeadersEditor
+ <Editor
+ language="json"
onChange={onHeadersChange}
{theme}
value={session.active?.headers ?? "{}"}/>
{/if}
</div>
</div>
- <Splitter
- onDrag={onLeftDrag}
- onDragStart={onLeftDragStart}
- onKeyAdjust={onLeftKeyAdjust}
- orientation="horizontal"/>
+
+ {#if windowWidth >= 1025}
+ <Splitter
+ onDrag={onLeftDrag}
+ onDragStart={onLeftDragStart}
+ onKeyAdjust={onLeftKeyAdjust}
+ orientation="horizontal"/>
+ {/if}
+
<div class="right">
<ResultViewer
footer={resultFooter}
@@ -550,6 +642,7 @@
value={session.active?.result ?? ""}/>
</div>
</div>
+
{#if docsOpen && schema.schema}
<Splitter
onDrag={onDocsDrag}
@@ -559,7 +652,8 @@
<DocExplorer schema={schema.schema}/>
{/if}
</div>
+
{#if schema.error}
- <div class="status">Schema error: {schema.error}</div>
+ <div class="status"><span>Schema error</span> {schema.error}</div>
{/if}
</div>