aboutsummaryrefslogtreecommitdiff
path: root/source/library/components/DocExplorer.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'source/library/components/DocExplorer.svelte')
-rw-r--r--source/library/components/DocExplorer.svelte226
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>