diff options
| author | netop://ウィビ <paul@webb.page> | 2026-04-24 11:33:25 -0700 |
|---|---|---|
| committer | netop://ウィビ <paul@webb.page> | 2026-04-24 11:33:25 -0700 |
| commit | 8a59f92d031963e23ecc84b75feecf43eb4dd146 (patch) | |
| tree | 75de5768885583897061a3b1795e4c987ce90039 /source/library/components/DocExplorer.svelte | |
| download | graphiql-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.svelte')
| -rw-r--r-- | source/library/components/DocExplorer.svelte | 226 |
1 files changed, 226 insertions, 0 deletions
diff --git a/source/library/components/DocExplorer.svelte b/source/library/components/DocExplorer.svelte new file mode 100644 index 0000000..536cb2a --- /dev/null +++ b/source/library/components/DocExplorer.svelte @@ -0,0 +1,226 @@ +<script lang="ts"> + import FieldView from "./DocExplorer/FieldView.svelte"; + import TypeView from "./DocExplorer/TypeView.svelte"; + import { + isInputObjectType, + isInterfaceType, + isObjectType + } from "graphql"; + import type { + GraphQLField, + GraphQLInputField, + GraphQLNamedType, + GraphQLSchema + } from "graphql"; + + type NavEntry = + | { kind: "field"; fieldName: string; typeName: string } + | { kind: "type"; name: string }; + + type Props = { + schema: GraphQLSchema; + }; + + let { schema }: Props = $props(); + + let stack = $state<NavEntry[]>([]); + + const current = $derived<NavEntry | null>(stack.length > 0 ? stack[stack.length - 1] : null); + + const rootTypes = $derived.by(() => { + const out: { label: string; type: GraphQLNamedType }[] = []; + const q = schema.getQueryType(); + const m = schema.getMutationType(); + const s = schema.getSubscriptionType(); + + if (q) + out.push({ label: "Query", type: q }); + + if (m) + out.push({ label: "Mutation", type: m }); + + if (s) + out.push({ label: "Subscription", type: s }); + + return out; + }); + + const currentType = $derived.by<GraphQLNamedType | null>(() => { + if (!current) + return null; + + const name = current.kind === "type" ? current.name : current.typeName; + return schema.getType(name) ?? null; + }); + + const currentField = $derived.by<GraphQLField<unknown, unknown> | GraphQLInputField | null>(() => { + if (!current || current.kind !== "field" || !currentType) + return null; + + if (isObjectType(currentType) || isInterfaceType(currentType) || isInputObjectType(currentType)) + return currentType.getFields()[current.fieldName] ?? null; + + return null; + }); + + function crumbLabel(entry: NavEntry): string { + return entry.kind === "type" ? entry.name : entry.fieldName; + } + + function gotoRoot() { + stack = []; + } + + function gotoIndex(index: number) { + stack = stack.slice(0, index + 1); + } + + function pushType(name: string) { + if (!schema.getType(name)) + return; + + stack = [...stack, { kind: "type", name }]; + } + + function pushField(fieldName: string) { + const typeName = current?.kind === "type" ? current.name : null; + + if (!typeName) + return; + + stack = [...stack, { fieldName, kind: "field", typeName }]; + } +</script> + +<style lang="scss"> + .explorer { + background: var(--graphiql-panel, #252526); + border-left: 1px solid var(--graphiql-border, #333); + display: grid; + grid-template-rows: auto 1fr; + height: 100%; + min-height: 0; + overflow: hidden; + } + + .breadcrumbs { + align-items: center; + border-bottom: 1px solid var(--graphiql-border, #333); + display: flex; + flex-wrap: wrap; + font-size: 0.8125rem; + gap: 0.25rem; + padding: 0.5rem 0.75rem; + } + + .crumb { + background: none; + border: none; + color: var(--graphiql-link, #79b8ff); + cursor: pointer; + font-family: inherit; + font-size: inherit; + padding: 0; + + &:hover { + text-decoration: underline; + } + + &.current { + color: var(--graphiql-fg, #d4d4d4); + cursor: default; + + &:hover { + text-decoration: none; + } + } + } + + .separator { + color: var(--graphiql-muted, #858585); + } + + .body { + min-height: 0; + overflow-y: auto; + } + + .root { + display: grid; + gap: 0.75rem; + padding: 0.75rem 1rem; + } + + .section-label { + color: var(--graphiql-muted, #858585); + font-size: 0.7rem; + letter-spacing: 0.05em; + margin-bottom: 0.25rem; + text-transform: uppercase; + } + + .root-list { + display: grid; + gap: 0.375rem; + } + + .root-link { + background: none; + border: none; + color: var(--graphiql-link, #79b8ff); + cursor: pointer; + font-family: inherit; + font-size: 0.875rem; + padding: 0; + text-align: left; + + &:hover { + text-decoration: underline; + } + } + + .empty { + color: var(--graphiql-muted, #858585); + font-size: 0.8125rem; + padding: 0.75rem 1rem; + } +</style> + +<div class="explorer"> + <div class="breadcrumbs"> + <button + class="crumb" + class:current={stack.length === 0} + onclick={gotoRoot}>Docs</button> + {#each stack as entry, i} + <span class="separator">/</span> + <button + class="crumb" + class:current={i === stack.length - 1} + onclick={() => gotoIndex(i)}>{crumbLabel(entry)}</button> + {/each} + </div> + <div class="body"> + {#if stack.length === 0} + <div class="root"> + <div class="section-label">Root Types</div> + <div class="root-list"> + {#each rootTypes as entry} + <button class="root-link" onclick={() => pushType(entry.type.name)}> + {entry.label}: {entry.type.name} + </button> + {/each} + </div> + </div> + {:else if currentField} + <FieldView field={currentField} onNavigate={pushType}/> + {:else if currentType} + <TypeView + onNavigateField={pushField} + onNavigateType={pushType} + type={currentType}/> + {:else} + <div class="empty">Type not found in schema.</div> + {/if} + </div> +</div> |