aboutsummaryrefslogtreecommitdiff
path: root/source/library/components/DocExplorer
diff options
context:
space:
mode:
authornetop://ウィビ <paul@webb.page>2026-04-24 11:33:25 -0700
committernetop://ウィビ <paul@webb.page>2026-04-24 11:33:25 -0700
commit8a59f92d031963e23ecc84b75feecf43eb4dd146 (patch)
tree75de5768885583897061a3b1795e4c987ce90039 /source/library/components/DocExplorer
downloadgraphiql-8a59f92d031963e23ecc84b75feecf43eb4dd146.tar.gz
graphiql-8a59f92d031963e23ecc84b75feecf43eb4dd146.zip
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
Diffstat (limited to 'source/library/components/DocExplorer')
-rw-r--r--source/library/components/DocExplorer/FieldView.svelte87
-rw-r--r--source/library/components/DocExplorer/TypeLink.svelte42
-rw-r--r--source/library/components/DocExplorer/TypeView.svelte199
3 files changed, 328 insertions, 0 deletions
diff --git a/source/library/components/DocExplorer/FieldView.svelte b/source/library/components/DocExplorer/FieldView.svelte
new file mode 100644
index 0000000..71d215c
--- /dev/null
+++ b/source/library/components/DocExplorer/FieldView.svelte
@@ -0,0 +1,87 @@
+<script lang="ts">
+ import TypeLink from "./TypeLink.svelte";
+ import type { GraphQLField, GraphQLInputField } from "graphql";
+
+ type Props = {
+ field: GraphQLField<unknown, unknown> | GraphQLInputField;
+ onNavigate: (typeName: string) => void;
+ };
+
+ let { field, onNavigate }: Props = $props();
+
+ const args = $derived("args" in field ? field.args : []);
+</script>
+
+<style lang="scss">
+ .field {
+ display: grid;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ }
+
+ .heading {
+ font-size: 0.95rem;
+ font-weight: 600;
+ }
+
+ .section-label {
+ color: var(--graphiql-muted, #858585);
+ font-size: 0.7rem;
+ letter-spacing: 0.05em;
+ margin-bottom: 0.25rem;
+ text-transform: uppercase;
+ }
+
+ .description {
+ color: var(--graphiql-muted, #858585);
+ font-size: 0.8125rem;
+ line-height: 1.4;
+ }
+
+ .args {
+ display: grid;
+ gap: 0.375rem;
+ }
+
+ .arg {
+ font-size: 0.8125rem;
+ }
+
+ .arg-name {
+ color: var(--graphiql-fg, #d4d4d4);
+ }
+
+ .arg-description {
+ color: var(--graphiql-muted, #858585);
+ font-size: 0.75rem;
+ margin-left: 1rem;
+ margin-top: 0.125rem;
+ }
+</style>
+
+<div class="field">
+ <div class="heading">{field.name}</div>
+ {#if field.description}
+ <div class="description">{field.description}</div>
+ {/if}
+ <div>
+ <div class="section-label">Type</div>
+ <TypeLink {onNavigate} type={field.type}/>
+ </div>
+ {#if args.length > 0}
+ <div>
+ <div class="section-label">Arguments</div>
+ <div class="args">
+ {#each args as arg}
+ <div class="arg">
+ <span class="arg-name">{arg.name}</span>:
+ <TypeLink {onNavigate} type={arg.type}/>
+ {#if arg.description}
+ <div class="arg-description">{arg.description}</div>
+ {/if}
+ </div>
+ {/each}
+ </div>
+ </div>
+ {/if}
+</div>
diff --git a/source/library/components/DocExplorer/TypeLink.svelte b/source/library/components/DocExplorer/TypeLink.svelte
new file mode 100644
index 0000000..253d16e
--- /dev/null
+++ b/source/library/components/DocExplorer/TypeLink.svelte
@@ -0,0 +1,42 @@
+<script lang="ts">
+ import { getNamedType, isListType, isNonNullType } from "graphql";
+ import type { GraphQLType } from "graphql";
+
+ type Props = {
+ onNavigate: (typeName: string) => void;
+ type: GraphQLType;
+ };
+
+ let { onNavigate, type }: Props = $props();
+
+ function label(t: GraphQLType): string {
+ if (isNonNullType(t))
+ return `${label(t.ofType)}!`;
+
+ if (isListType(t))
+ return `[${label(t.ofType)}]`;
+
+ return t.name;
+ }
+
+ const named = $derived(getNamedType(type));
+ const text = $derived(label(type));
+</script>
+
+<style lang="scss">
+ .link {
+ background: none;
+ border: none;
+ color: var(--graphiql-link, #79b8ff);
+ cursor: pointer;
+ font-family: inherit;
+ font-size: inherit;
+ padding: 0;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+</style>
+
+<button class="link" onclick={() => onNavigate(named.name)}>{text}</button>
diff --git a/source/library/components/DocExplorer/TypeView.svelte b/source/library/components/DocExplorer/TypeView.svelte
new file mode 100644
index 0000000..31a1ca3
--- /dev/null
+++ b/source/library/components/DocExplorer/TypeView.svelte
@@ -0,0 +1,199 @@
+<script lang="ts">
+ import TypeLink from "./TypeLink.svelte";
+ import {
+ isEnumType,
+ isInputObjectType,
+ isInterfaceType,
+ isObjectType,
+ isScalarType,
+ isUnionType
+ } from "graphql";
+ import type { GraphQLNamedType } from "graphql";
+
+ type Props = {
+ onNavigateField: (fieldName: string) => void;
+ onNavigateType: (typeName: string) => void;
+ type: GraphQLNamedType;
+ };
+
+ let { onNavigateField, onNavigateType, type }: Props = $props();
+
+ const fields = $derived.by(() => {
+ if (isObjectType(type) || isInterfaceType(type) || isInputObjectType(type))
+ return Object.values(type.getFields());
+
+ return [];
+ });
+
+ const interfaces = $derived.by(() => {
+ if (isObjectType(type) || isInterfaceType(type))
+ return type.getInterfaces();
+
+ return [];
+ });
+
+ const unionMembers = $derived.by(() => {
+ if (isUnionType(type))
+ return type.getTypes();
+
+ return [];
+ });
+
+ const enumValues = $derived.by(() => {
+ if (isEnumType(type))
+ return type.getValues();
+
+ return [];
+ });
+
+ const kindLabel = $derived.by(() => {
+ if (isObjectType(type))
+ return "type";
+
+ if (isInterfaceType(type))
+ return "interface";
+
+ if (isUnionType(type))
+ return "union";
+
+ if (isEnumType(type))
+ return "enum";
+
+ if (isInputObjectType(type))
+ return "input";
+
+ if (isScalarType(type))
+ return "scalar";
+
+ return "";
+ });
+</script>
+
+<style lang="scss">
+ .type {
+ display: grid;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ }
+
+ .heading {
+ font-size: 0.95rem;
+ font-weight: 600;
+ }
+
+ .kind {
+ color: var(--graphiql-muted, #858585);
+ font-weight: normal;
+ margin-right: 0.375rem;
+ }
+
+ .description {
+ color: var(--graphiql-muted, #858585);
+ font-size: 0.8125rem;
+ line-height: 1.4;
+ }
+
+ .section-label {
+ color: var(--graphiql-muted, #858585);
+ font-size: 0.7rem;
+ letter-spacing: 0.05em;
+ margin-bottom: 0.25rem;
+ text-transform: uppercase;
+ }
+
+ .list {
+ display: grid;
+ gap: 0.375rem;
+ }
+
+ .entry {
+ font-size: 0.8125rem;
+ }
+
+ .field-button {
+ background: none;
+ border: none;
+ color: var(--graphiql-fg, #d4d4d4);
+ cursor: pointer;
+ font-family: inherit;
+ font-size: inherit;
+ padding: 0;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .entry-description {
+ color: var(--graphiql-muted, #858585);
+ font-size: 0.75rem;
+ margin-left: 1rem;
+ margin-top: 0.125rem;
+ }
+</style>
+
+<div class="type">
+ <div class="heading">
+ {#if kindLabel}<span class="kind">{kindLabel}</span>{/if}{type.name}
+ </div>
+ {#if type.description}
+ <div class="description">{type.description}</div>
+ {/if}
+ {#if interfaces.length > 0}
+ <div>
+ <div class="section-label">Implements</div>
+ <div class="list">
+ {#each interfaces as iface}
+ <div class="entry">
+ <TypeLink onNavigate={onNavigateType} type={iface}/>
+ </div>
+ {/each}
+ </div>
+ </div>
+ {/if}
+ {#if fields.length > 0}
+ <div>
+ <div class="section-label">Fields</div>
+ <div class="list">
+ {#each fields as field}
+ <div class="entry">
+ <button
+ class="field-button"
+ onclick={() => onNavigateField(field.name)}>{field.name}</button>:
+ <TypeLink onNavigate={onNavigateType} type={field.type}/>
+ {#if field.description}
+ <div class="entry-description">{field.description}</div>
+ {/if}
+ </div>
+ {/each}
+ </div>
+ </div>
+ {/if}
+ {#if unionMembers.length > 0}
+ <div>
+ <div class="section-label">Members</div>
+ <div class="list">
+ {#each unionMembers as member}
+ <div class="entry">
+ <TypeLink onNavigate={onNavigateType} type={member}/>
+ </div>
+ {/each}
+ </div>
+ </div>
+ {/if}
+ {#if enumValues.length > 0}
+ <div>
+ <div class="section-label">Values</div>
+ <div class="list">
+ {#each enumValues as value}
+ <div class="entry">
+ <span>{value.name}</span>
+ {#if value.description}
+ <div class="entry-description">{value.description}</div>
+ {/if}
+ </div>
+ {/each}
+ </div>
+ </div>
+ {/if}
+</div>