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 | |
| 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')
| -rw-r--r-- | source/library/components/DocExplorer/FieldView.svelte | 87 | ||||
| -rw-r--r-- | source/library/components/DocExplorer/TypeLink.svelte | 42 | ||||
| -rw-r--r-- | source/library/components/DocExplorer/TypeView.svelte | 199 |
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> |