diff options
Diffstat (limited to 'source/library')
23 files changed, 1056 insertions, 757 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> diff --git a/source/library/components/DocExplorer.svelte b/source/library/components/DocExplorer.svelte index 536cb2a..48525e1 100644 --- a/source/library/components/DocExplorer.svelte +++ b/source/library/components/DocExplorer.svelte @@ -1,18 +1,19 @@ <script lang="ts"> - import FieldView from "./DocExplorer/FieldView.svelte"; - import TypeView from "./DocExplorer/TypeView.svelte"; + /*** IMPORT ------------------------------------------- ***/ import { isInputObjectType, isInterfaceType, - isObjectType - } from "graphql"; - import type { - GraphQLField, - GraphQLInputField, - GraphQLNamedType, - GraphQLSchema + isObjectType, + type GraphQLField, + type GraphQLInputField, + type GraphQLNamedType, + type GraphQLSchema } from "graphql"; + /*** UTILITY ------------------------------------------ ***/ + import FieldView from "./DocExplorer/FieldView.svelte"; + import TypeView from "./DocExplorer/TypeView.svelte"; + type NavEntry = | { kind: "field"; fieldName: string; typeName: string } | { kind: "type"; name: string }; @@ -22,7 +23,6 @@ }; let { schema }: Props = $props(); - let stack = $state<NavEntry[]>([]); const current = $derived<NavEntry | null>(stack.length > 0 ? stack[stack.length - 1] : null); @@ -63,23 +63,17 @@ return null; }); + /*** HELPER ------------------------------------------- ***/ 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 gotoRoot() { + stack = []; } function pushField(fieldName: string) { @@ -90,54 +84,62 @@ stack = [...stack, { fieldName, kind: "field", typeName }]; } + + function pushType(name: string) { + if (!schema.getType(name)) + return; + + stack = [...stack, { kind: "type", name }]; + } </script> <style lang="scss"> .explorer { - background: var(--graphiql-panel, #252526); - border-left: 1px solid var(--graphiql-border, #333); + background-color: color-mix(in oklch shorter hue, var(--uchu-gray-1) 20%, var(--uchu-yang) 80%); + border-left: 1px solid var(--uchu-gray-2); display: grid; grid-template-rows: auto 1fr; height: 100%; min-height: 0; overflow: hidden; + z-index: 1; } .breadcrumbs { align-items: center; - border-bottom: 1px solid var(--graphiql-border, #333); + border-bottom: 1px solid var(--uchu-gray-2); 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; - } + .crumb { + background: none; + border: none; + cursor: pointer; + font-family: inherit; + font-size: inherit; + padding: 0; + + &:not(.current) { + color: var(--uchu-blue-3); + text-decoration: underline; + } - &.current { - color: var(--graphiql-fg, #d4d4d4); - cursor: default; + &.current { + color: var(--uchu-yin-4); + cursor: default; - &:hover { - text-decoration: none; + &:hover { + text-decoration: none; + } } } } .separator { - color: var(--graphiql-muted, #858585); + color: var(--uchu-gray-3); } .body { @@ -149,39 +151,39 @@ display: grid; gap: 0.75rem; padding: 0.75rem 1rem; + + &-list { + display: grid; + gap: 0.375rem; + } + + &-link { + background: none; + border: none; + color: var(--uchu-blue-3); + cursor: pointer; + font-family: inherit; + font-size: 0.95rem; + padding: 0; + text-align: left; + + &:hover { + text-decoration: underline; + } + } } .section-label { - color: var(--graphiql-muted, #858585); + color: var(--uchu-yin-6); font-size: 0.7rem; - letter-spacing: 0.05em; + letter-spacing: 0.05rem; 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; + color: var(--uchu-yin-6); + font-size: 0.8rem; padding: 0.75rem 1rem; } </style> @@ -200,10 +202,12 @@ 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)}> diff --git a/source/library/components/DocExplorer/FieldView.svelte b/source/library/components/DocExplorer/FieldView.svelte index 71d215c..1cc62ae 100644 --- a/source/library/components/DocExplorer/FieldView.svelte +++ b/source/library/components/DocExplorer/FieldView.svelte @@ -1,7 +1,11 @@ <script lang="ts"> - import TypeLink from "./TypeLink.svelte"; + /*** IMPORT ------------------------------------------- ***/ import type { GraphQLField, GraphQLInputField } from "graphql"; + /*** UTILITY ------------------------------------------ ***/ + import { markdown } from "../../graphql/markdown.ts"; + import TypeLink from "./TypeLink.svelte"; + type Props = { field: GraphQLField<unknown, unknown> | GraphQLInputField; onNavigate: (typeName: string) => void; @@ -15,7 +19,7 @@ <style lang="scss"> .field { display: grid; - gap: 0.75rem; + gap: 1.5rem; padding: 0.75rem 1rem; } @@ -24,60 +28,62 @@ font-weight: 600; } + .section-label, + .description, + .arg-description { + color: var(--uchu-yin-6); + } + .section-label { - color: var(--graphiql-muted, #858585); font-size: 0.7rem; - letter-spacing: 0.05em; + letter-spacing: 0.05rem; margin-bottom: 0.25rem; text-transform: uppercase; } .description { - color: var(--graphiql-muted, #858585); - font-size: 0.8125rem; + font-size: 0.8rem; line-height: 1.4; } .args { display: grid; - gap: 0.375rem; + gap: 1.5rem; } .arg { - font-size: 0.8125rem; - } + font-size: 0.8rem; - .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; + &-description { + font-size: 0.7rem; + 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> + <div class="description">{@html markdown(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> + <div class="arg-description">{@html markdown(arg.description)}</div> {/if} </div> {/each} diff --git a/source/library/components/DocExplorer/TypeLink.svelte b/source/library/components/DocExplorer/TypeLink.svelte index 253d16e..03f5c1e 100644 --- a/source/library/components/DocExplorer/TypeLink.svelte +++ b/source/library/components/DocExplorer/TypeLink.svelte @@ -1,7 +1,13 @@ <script lang="ts"> - import { getNamedType, isListType, isNonNullType } from "graphql"; - import type { GraphQLType } from "graphql"; - + /*** IMPORT ------------------------------------------- ***/ + import { + getNamedType, + isListType, + isNonNullType, + type GraphQLType + } from "graphql"; + + /*** UTILITY ------------------------------------------ ***/ type Props = { onNavigate: (typeName: string) => void; type: GraphQLType; @@ -27,13 +33,13 @@ .link { background: none; border: none; - color: var(--graphiql-link, #79b8ff); + color: var(--uchu-blue-3); cursor: pointer; font-family: inherit; font-size: inherit; padding: 0; - &:hover { + &:not(:hover) { text-decoration: underline; } } diff --git a/source/library/components/DocExplorer/TypeView.svelte b/source/library/components/DocExplorer/TypeView.svelte index 31a1ca3..c1978d6 100644 --- a/source/library/components/DocExplorer/TypeView.svelte +++ b/source/library/components/DocExplorer/TypeView.svelte @@ -1,14 +1,18 @@ <script lang="ts"> - import TypeLink from "./TypeLink.svelte"; + /*** IMPORT ------------------------------------------- ***/ import { isEnumType, isInputObjectType, isInterfaceType, isObjectType, isScalarType, - isUnionType + isUnionType, + type GraphQLNamedType } from "graphql"; - import type { GraphQLNamedType } from "graphql"; + + /*** UTILITY ------------------------------------------ ***/ + import { markdown } from "../../graphql/markdown.ts"; + import TypeLink from "./TypeLink.svelte"; type Props = { onNavigateField: (fieldName: string) => void; @@ -72,7 +76,7 @@ <style lang="scss"> .type { display: grid; - gap: 0.75rem; + gap: 1.5rem; padding: 0.75rem 1rem; } @@ -81,67 +85,80 @@ font-weight: 600; } + .kind, + .description, + .entry-description { + color: var(--uchu-yin-6); + } + .kind { - color: var(--graphiql-muted, #858585); font-weight: normal; margin-right: 0.375rem; } .description { - color: var(--graphiql-muted, #858585); - font-size: 0.8125rem; + font-size: 0.8rem; line-height: 1.4; + + :global(a) { + background-color: oklch(var(--uchu-blue-1-raw) / 25%); + color: var(--uchu-blue-4); + } + + :global(code) { + background-color: var(--uchu-yellow-1); + } } .section-label { - color: var(--graphiql-muted, #858585); font-size: 0.7rem; - letter-spacing: 0.05em; + letter-spacing: 0.05rem; margin-bottom: 0.25rem; text-transform: uppercase; } .list { display: grid; - gap: 0.375rem; + gap: 1.5rem; } .entry { - font-size: 0.8125rem; + font-size: 0.8rem; + + &-description { + font-size: 0.7rem; + margin-left: 1rem; + margin-top: 0.125rem; + } } .field-button { background: none; border: none; - color: var(--graphiql-fg, #d4d4d4); cursor: pointer; font-family: inherit; font-size: inherit; padding: 0; - &:hover { + &:not(: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> + <div class="description">{@html markdown(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"> @@ -151,9 +168,11 @@ </div> </div> {/if} + {#if fields.length > 0} <div> <div class="section-label">Fields</div> + <div class="list"> {#each fields as field} <div class="entry"> @@ -161,17 +180,20 @@ 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> + <div class="entry-description">{@html markdown(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"> @@ -181,15 +203,18 @@ </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> + <div class="entry-description">{@html markdown(value.description)}</div> {/if} </div> {/each} diff --git a/source/library/components/Editor.svelte b/source/library/components/Editor.svelte index 073a76a..77ef5ea 100644 --- a/source/library/components/Editor.svelte +++ b/source/library/components/Editor.svelte @@ -1,12 +1,12 @@ <script lang="ts"> /*** IMPORT ------------------------------------------- ***/ - import { onMount } from "svelte"; - import type { Extension } from "@codemirror/state"; import type { EditorView } from "@codemirror/view"; + import type { Extension } from "@codemirror/state"; import type { GraphQLSchema } from "graphql"; /*** UTILITY ------------------------------------------ ***/ + import { lightTheme } from "../themes/light.ts"; type Props = { language?: "graphql" | "json"; @@ -61,7 +61,7 @@ if (disposed) return; - const themeExt: Extension = theme ?? (await import("@codemirror/theme-one-dark")).oneDark; + const themeExt: Extension = theme ?? lightTheme; const languageExt = language === "graphql" ? graphql(schema ? buildSchema(schema) : undefined) : @@ -131,8 +131,7 @@ <style lang="scss"> .editor { - height: 100%; - width: 100%; + width: 100%; height: 100%; :global(.cm-editor) { height: 100%; diff --git a/source/library/components/HeadersEditor.svelte b/source/library/components/HeadersEditor.svelte deleted file mode 100644 index fc3a193..0000000 --- a/source/library/components/HeadersEditor.svelte +++ /dev/null @@ -1,34 +0,0 @@ -<script lang="ts"> - import Editor from "./Editor.svelte"; - import type { Extension } from "@codemirror/state"; - - type Props = { - onChange: (value: string) => void; - theme?: Extension; - value: string; - }; - - let { onChange, theme, value }: Props = $props(); -</script> - -<style lang="scss"> - .headers { - display: grid; - grid-template-rows: auto 1fr; - height: 100%; - min-height: 0; - } - - .label { - background-color: var(--graphiql-panel, #252526); - font-size: 0.75rem; - letter-spacing: 0.05em; - padding: 0.25rem 0.75rem; - text-transform: uppercase; - } -</style> - -<div class="headers"> - <div class="label">Headers</div> - <Editor language="json" {onChange} {theme} {value}/> -</div> diff --git a/source/library/components/HistoryPanel.svelte b/source/library/components/HistoryPanel.svelte index 01f397a..e224e1e 100644 --- a/source/library/components/HistoryPanel.svelte +++ b/source/library/components/HistoryPanel.svelte @@ -1,4 +1,5 @@ <script lang="ts"> + /*** UTILITY ------------------------------------------ ***/ import type { HistoryEntry } from "../state/history.svelte.ts"; type Props = { @@ -34,6 +35,7 @@ return b.timestamp - a.timestamp; })); + /*** HELPER ------------------------------------------- ***/ function formatTimestamp(ms: number): string { const d = new Date(ms); return d.toLocaleString(); @@ -50,10 +52,6 @@ } } - function onImportClick() { - fileInput?.click(); - } - function onFileChange(event: Event) { const input = event.currentTarget as HTMLInputElement; const file = input.files?.[0]; @@ -63,22 +61,29 @@ input.value = ""; } + + function onImportClick() { + fileInput?.click(); + } </script> <style lang="scss"> .panel { - background: var(--graphiql-panel, #252526); - border-right: 1px solid var(--graphiql-border, #333); + background-color: var(--uchu-yin-9); + color: var(--uchu-yin-1); display: grid; + font-size: 0.8rem; grid-template-rows: auto 1fr; - height: 100%; min-height: 0; + height: 100%; overflow: hidden; + z-index: 2; } - .header { + .header, + .notice { align-items: center; - border-bottom: 1px solid var(--graphiql-border, #333); + border-bottom: 1px solid var(--uchu-yin-8); display: flex; gap: 0.5rem; justify-content: space-between; @@ -86,26 +91,32 @@ } .title { - font-size: 0.8125rem; font-weight: 600; } .actions { display: flex; gap: 0.5rem; + + .action { + background: none; + border: none; + cursor: pointer; + font-family: inherit; + font-size: 0.75rem; + padding: 0; + } } - .action { - background: none; - border: none; - color: var(--graphiql-muted, #858585); - cursor: pointer; - font-family: inherit; - font-size: 0.75rem; - padding: 0; + .action, + .notice-dismiss, + .remove { + &:not(:hover) { + color: var(--uchu-yin-5); + } &:hover { - color: var(--graphiql-fg, #d4d4d4); + color: var(--uchu-yin-1); } } @@ -113,38 +124,20 @@ display: none; } - .notice { - align-items: center; - background: var(--graphiql-bg, #1e1e1e); - border-bottom: 1px solid var(--graphiql-border, #333); - color: var(--graphiql-fg, #d4d4d4); - display: flex; - font-size: 0.75rem; - gap: 0.5rem; - justify-content: space-between; - padding: 0.375rem 0.75rem; - } - - .notice-dismiss { + .notice-dismiss, + .remove { background: none; border: none; - color: var(--graphiql-muted, #858585); cursor: pointer; - font-size: 0.875rem; + font-size: 1rem; line-height: 1; padding: 0 0.25rem; - - &:hover { - color: var(--graphiql-fg, #d4d4d4); - } } .list { display: grid; - gap: 0.125rem; min-height: 0; overflow-y: auto; - padding: 0.375rem 0; } .entry { @@ -154,22 +147,56 @@ gap: 0.125rem; grid-template-columns: auto 1fr auto; padding: 0.375rem 0.75rem; + transition: opacity 0.1s; + + &:not(:last-of-type) { + border-bottom: 1px solid var(--uchu-yin); + } &:hover { - background: var(--graphiql-bg, #1e1e1e); + background-color: var(--uchu-yin); + } + + .list:has(.entry:hover) &:not(:hover) { + opacity: 0.4; + } + + &-title { + font-size: 0.8rem; + margin-bottom: 0.1rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &-time { + color: var(--uchu-yin-5); + font-size: 0.7rem; } } .star { + align-self: center; background: none; border: none; - color: var(--graphiql-muted, #858585); cursor: pointer; - font-size: 0.875rem; + font-size: 0.8rem; padding: 0 0.375rem 0 0; + &:not(.active) { + color: var(--uchu-yin-5); + } + &.active { - color: var(--graphiql-accent, #e3b341); + color: var(--uchu-orange-4); + + svg { + fill: currentColor; + } + } + + svg { + width: 1rem; } } @@ -178,75 +205,55 @@ min-width: 0; } - .entry-title { - font-size: 0.8125rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .entry-time { - color: var(--graphiql-muted, #858585); - font-size: 0.7rem; - } - .remove { align-self: center; - background: none; - border: none; - color: var(--graphiql-muted, #858585); - cursor: pointer; - font-size: 1rem; - line-height: 1; - padding: 0 0.25rem; - - &:hover { - color: var(--graphiql-fg, #d4d4d4); - } } .empty { - color: var(--graphiql-muted, #858585); - font-size: 0.8125rem; + color: var(--uchu-yin-5); + font-size: 0.8rem; padding: 0.75rem; } </style> <div class="panel"> - <div> - <div class="header"> - <span class="title">History</span> - <div class="actions"> - {#if onExport} - <button class="action" onclick={onExport} type="button">Export</button> - {/if} - {#if onImport} - <button class="action" onclick={onImportClick} type="button">Import</button> - <input - accept="application/json" - bind:this={fileInput} - class="hidden-input" - onchange={onFileChange} - type="file"/> - {/if} - {#if entries.length > 0} - <button class="action" onclick={onClear} type="button">Clear</button> - {/if} - </div> + <div class="header"> + <span class="title">History</span> + + <div class="actions"> + {#if onExport} + <button class="action" onclick={onExport} type="button">Export</button> + {/if} + + {#if onImport} + <button class="action" onclick={onImportClick} type="button">Import</button> + <input + accept="application/json" + bind:this={fileInput} + class="hidden-input" + onchange={onFileChange} + type="file"/> + {/if} + + {#if entries.length > 0} + <button class="action" onclick={onClear} type="button">Clear</button> + {/if} </div> - {#if notice} - <div class="notice"> - <span>{notice}</span> - {#if onDismissNotice} - <button - aria-label="Dismiss notice" - class="notice-dismiss" - onclick={onDismissNotice} - type="button">×</button> - {/if} - </div> - {/if} </div> + + {#if notice} + <div class="notice"> + <span>{notice}</span> + {#if onDismissNotice} + <button + aria-label="Dismiss notice" + class="notice-dismiss" + onclick={onDismissNotice} + type="button">×</button> + {/if} + </div> + {/if} + <div class="list"> {#if sorted.length === 0} <div class="empty">No history yet.</div> @@ -264,16 +271,28 @@ class="star" class:active={entry.favorite} onclick={(e) => { e.stopPropagation(); onFavorite(entry.id); }} - type="button">{entry.favorite ? "★" : "☆"}</button> + type="button"> + {#if entry.favorite} + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M12 2L14.9389 7.95492L21.5106 8.90983L16.7553 13.5451L17.8779 20.0902L12 17L6.12215 20.0902L7.24472 13.5451L2.48944 8.90983L9.06108 7.95492L12 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> + </svg> + {:else} + <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <path d="M12 2L14.9389 7.95492L21.5106 8.90983L16.7553 13.5451L17.8779 20.0902L12 17L6.12215 20.0902L7.24472 13.5451L2.48944 8.90983L9.06108 7.95492L12 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> + </svg> + {/if} + </button> + <div class="meta"> <div class="entry-title">{entry.title}</div> <div class="entry-time">{formatTimestamp(entry.timestamp)}</div> </div> + <button aria-label="Remove entry" class="remove" onclick={(e) => { e.stopPropagation(); onRemove(entry.id); }} - type="button">×</button> + type="button">×</button> </div> {/each} {/if} diff --git a/source/library/components/ResultViewer.svelte b/source/library/components/ResultViewer.svelte index d277ec8..270965b 100644 --- a/source/library/components/ResultViewer.svelte +++ b/source/library/components/ResultViewer.svelte @@ -1,7 +1,10 @@ <script lang="ts"> - import Editor from "./Editor.svelte"; + /*** IMPORT ------------------------------------------- ***/ import type { Extension } from "@codemirror/state"; import type { Snippet } from "svelte"; + + /*** UTILITY ------------------------------------------ ***/ + import Editor from "./Editor.svelte"; import type { TabTiming } from "../state/session.svelte.ts"; type Props = { @@ -20,18 +23,6 @@ value }: Props = $props(); - function median(values: number[]): number { - if (values.length === 0) - return 0; - - const sorted = [...values].sort((a, b) => a - b); - const mid = Math.floor(sorted.length / 2); - - return sorted.length % 2 === 0 ? - (sorted[mid - 1] + sorted[mid]) / 2 : - sorted[mid]; - } - let metadata = $derived.by(() => { if (!timing) return null; @@ -39,55 +30,101 @@ if (streamIntervals.length > 0) { const medianMs = Math.round(median(streamIntervals)); const messages = streamIntervals.length + 1; - return `→ ${messages} messages · median ${medianMs}ms`; + return `${messages} messages · median ${medianMs}ms`; } const totalMs = Math.round(timing.endMs - timing.startMs); const firstByteMs = Math.round(timing.firstByteMs - timing.startMs); - return `→ ${totalMs}ms · first byte ${firstByteMs}ms`; + return `${totalMs}ms · first byte ${firstByteMs}ms`; }); + /*** HELPER ------------------------------------------- ***/ + function median(values: number[]): number { + if (values.length === 0) + return 0; + + const sorted = [...values].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + + return sorted.length % 2 === 0 ? + (sorted[mid - 1] + sorted[mid]) / 2 : + sorted[mid]; + } + function noop(_v: string) {} </script> <style lang="scss"> .result { - display: grid; - grid-template-rows: auto 1fr auto auto; + display: flex; + flex-direction: column; height: 100%; min-height: 0; + position: relative; } - .label { - background: var(--graphiql-panel, #252526); + .label, + .metadata, + .footer { font-size: 0.75rem; - letter-spacing: 0.05em; padding: 0.25rem 0.75rem; + } + + .label, + .metadata { + background-color: var(--uchu-gray-2); + color: var(--uchu-yin-7); + } + + .label { + top: 1rem; right: 1rem; + + border: 1px solid var(--uchu-gray-3); + font-weight: 600; + letter-spacing: 0.05rem; + pointer-events: none; + position: absolute; text-transform: uppercase; + z-index: 1; + } + + .metadata, + .footer { + position: relative; } .metadata { - background: var(--graphiql-panel, #252526); - border-top: 1px solid var(--graphiql-border, #333); - color: var(--graphiql-muted, #858585); - font-size: 0.75rem; - padding: 0.25rem 0.75rem; + align-items: center; + display: flex; + flex-direction: row; + + svg { + width: 1rem; height: 1rem; + margin-right: 0.25rem; + } } .footer { - background: var(--graphiql-panel, #252526); - border-top: 1px solid var(--graphiql-border, #333); - font-size: 0.75rem; - padding: 0.25rem 0.75rem; + background-color: var(--uchu-gray-3); + border-top: 1px solid var(--uchu-gray-4); } </style> <div class="result"> <div class="label">Response</div> + <Editor language="json" onChange={noop} readOnly {theme} {value}/> + {#if metadata} - <div class="metadata">{metadata}</div> + <div class="metadata"> + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M14 5.75L20.25 12L14 18.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="square"/> + <path d="M19.5 12H3.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="square"/> + </svg> + {@html metadata} + </div> {/if} + {#if footer} <div class="footer">{@render footer({ result: value })}</div> {/if} diff --git a/source/library/components/Splitter.svelte b/source/library/components/Splitter.svelte index 73d0e10..a758385 100644 --- a/source/library/components/Splitter.svelte +++ b/source/library/components/Splitter.svelte @@ -1,4 +1,5 @@ <script lang="ts"> + /*** UTILITY ------------------------------------------ ***/ type Props = { label?: string; max?: number; @@ -27,33 +28,7 @@ let startX = $state(0); let startY = $state(0); - function onPointerDown(event: PointerEvent) { - event.preventDefault(); - dragging = true; - startX = event.clientX; - startY = event.clientY; - event.currentTarget instanceof Element && - event.currentTarget.setPointerCapture(event.pointerId); - onDragStart?.(); - } - - function onPointerMove(event: PointerEvent) { - if (!dragging) - return; - - onDrag(event.clientX - startX, event.clientY - startY); - } - - function onPointerUp(event: PointerEvent) { - if (!dragging) - return; - - dragging = false; - event.currentTarget instanceof Element && - event.currentTarget.releasePointerCapture(event.pointerId); - onDragEnd?.(); - } - + /*** HELPER ------------------------------------------- ***/ function onKeydown(event: KeyboardEvent) { if (!onKeyAdjust) return; @@ -76,49 +51,82 @@ } } } + + function onPointerDown(event: PointerEvent) { + event.preventDefault(); + dragging = true; + startX = event.clientX; + startY = event.clientY; + event.currentTarget instanceof Element && event.currentTarget.setPointerCapture(event.pointerId); + onDragStart?.(); + } + + function onPointerMove(event: PointerEvent) { + if (!dragging) + return; + + onDrag(event.clientX - startX, event.clientY - startY); + } + + function onPointerUp(event: PointerEvent) { + if (!dragging) + return; + + dragging = false; + event.currentTarget instanceof Element && event.currentTarget.releasePointerCapture(event.pointerId); + onDragEnd?.(); + } </script> <style lang="scss"> .splitter { align-items: center; - background: transparent; display: flex; justify-content: center; position: relative; touch-action: none; user-select: none; - &:hover::after, - &.dragging::after { - background: var(--graphiql-accent, #0e639c); - } - &::after { - background: var(--graphiql-border, #333); + border-radius: 25rem; content: ""; position: absolute; transition: background 120ms ease; } + &:not(:hover):not(.dragging) { + background-color: transparent; + + &::after { + background-color: inherit; + } + } + + &:hover, + &.dragging { + background-color: oklch(var(--uchu-gray-2-raw) / 25%); + + &::after, + &::after { + background-color: var(--uchu-orange-4); + } + } + &.horizontal { + width: 6px; height: 100%; cursor: col-resize; - height: 100%; - width: 6px; &::after { - height: 100%; - width: 1px; + width: 3px; height: 60%; } } &.vertical { + width: 100%; height: 6px; cursor: row-resize; - height: 6px; - width: 100%; &::after { - height: 1px; - width: 100%; + width: 60%; height: 3px; } } } @@ -139,5 +147,4 @@ onpointermove={onPointerMove} onpointerup={onPointerUp} role="separator" - tabindex="0" -></div> + tabindex="0"></div> diff --git a/source/library/components/TabBar.svelte b/source/library/components/TabBar.svelte index 0bfec02..547fa44 100644 --- a/source/library/components/TabBar.svelte +++ b/source/library/components/TabBar.svelte @@ -1,5 +1,8 @@ <script lang="ts"> + /*** IMPORT ------------------------------------------- ***/ import { tick, type Snippet } from "svelte"; + + /*** UTILITY ------------------------------------------ ***/ import type { Tab } from "../state/session.svelte.ts"; type Props = { @@ -12,29 +15,38 @@ tabs: Tab[]; }; - let { activeId, extras, onAdd, onClose, onRename, onSelect, tabs }: Props = $props(); + let { + activeId, + extras, + onAdd, + onClose, + onRename, + onSelect, + tabs + }: Props = $props(); - let editingId = $state<string | null>(null); let draft = $state<string>(""); + let editingId = $state<string | null>(null); let inputEl = $state<HTMLInputElement | null>(null); - async function startEditing(tab: Tab) { - editingId = tab.id; - draft = tab.title; - await tick(); - inputEl?.select(); + /*** HELPER ------------------------------------------- ***/ + function cancel() { + editingId = null; + draft = ""; } function commit() { - if (editingId === null) return; + if (editingId === null) + return; + onRename(editingId, draft); editingId = null; draft = ""; } - function cancel() { - editingId = null; - draft = ""; + function handleClose(event: MouseEvent, id: string) { + event.stopPropagation(); + onClose(id); } function onKeydown(event: KeyboardEvent) { @@ -47,41 +59,47 @@ } } - function handleClose(event: MouseEvent, id: string) { - event.stopPropagation(); - onClose(id); + async function startEditing(tab: Tab) { + editingId = tab.id; + draft = tab.title; + await tick(); + inputEl?.select(); } </script> <style lang="scss"> .tabbar { align-items: stretch; - background: var(--graphiql-panel, #252526); - border-bottom: 1px solid var(--graphiql-border, #333); + background-color: var(--uchu-gray-2); display: flex; - font-size: 0.8125rem; + font-size: 0.8rem; min-height: 2rem; overflow-x: auto; - } - - .tab { - align-items: center; - background: transparent; - border: none; - border-right: 1px solid var(--graphiql-border, #333); - color: var(--graphiql-muted, #858585); - cursor: pointer; - display: flex; - gap: 0.5rem; - padding: 0 0.75rem; - - &.active { - background: var(--graphiql-bg, #1e1e1e); - color: var(--graphiql-fg, #d4d4d4); - } - &:hover:not(.active) { - color: var(--graphiql-fg, #d4d4d4); + .tab { + align-items: center; + border: none; + cursor: pointer; + display: flex; + gap: 0.5rem; + padding: 0 0.75rem; + + &:not(:hover):not(.active) { + color: var(--uchu-yin-5); + } + + &:hover:not(.active) { + color: var(--uchu-yin-8); + } + + &:not(.active) { + background-color: transparent; + } + + &.active { + background-color: var(--uchu-gray-3); + color: var(--uchu-yin-8); + } } } @@ -90,10 +108,9 @@ } .edit { - background: var(--graphiql-bg, #1e1e1e); - border: 1px solid var(--graphiql-accent, #0e639c); - border-radius: 2px; - color: var(--graphiql-fg, #d4d4d4); + background-color: var(--uchu-orange-5); + border: 1px solid var(--uchu-orange-5); + color: var(--uchu-yang); font-family: inherit; font-size: inherit; min-width: 6rem; @@ -108,9 +125,12 @@ cursor: pointer; font-size: 1rem; line-height: 1; - opacity: 0.6; padding: 0; + &:not(:hover) { + opacity: 0.6; + } + &:hover { opacity: 1; } @@ -119,13 +139,17 @@ .add { background: none; border: none; - color: var(--graphiql-muted, #858585); + border-left: 1px solid var(--uchu-gray-3); cursor: pointer; font-size: 1rem; padding: 0 0.75rem; + &:not(:hover) { + color: var(--uchu-yin-5); + } + &:hover { - color: var(--graphiql-fg, #d4d4d4); + color: var(--uchu-yin-8); } } </style> @@ -145,8 +169,7 @@ } }} role="tab" - tabindex="0" - > + tabindex="0"> {#if editingId === tab.id} <input bind:this={inputEl} @@ -155,18 +178,19 @@ onblur={commit} onclick={(e) => e.stopPropagation()} onkeydown={onKeydown} - type="text" - /> + type="text"/> {:else} <span class="title">{tab.title}</span> {/if} + {#if extras}{@render extras({ tab })}{/if} + <button aria-label="Close tab" class="close" - onclick={(e) => handleClose(e, tab.id)} - >×</button> + onclick={(e) => handleClose(e, tab.id)}>×</button> </div> {/each} + <button aria-label="New tab" class="add" onclick={onAdd}>+</button> </div> diff --git a/source/library/components/Toolbar.svelte b/source/library/components/Toolbar.svelte index 9882b7d..37cc7ce 100644 --- a/source/library/components/Toolbar.svelte +++ b/source/library/components/Toolbar.svelte @@ -1,7 +1,10 @@ <script lang="ts"> - import type { OperationInfo } from "../graphql/operations.ts"; + /*** IMPORT ------------------------------------------- ***/ import type { Snippet } from "svelte"; + /*** UTILITY ------------------------------------------ ***/ + import type { OperationInfo } from "../graphql/operations.ts"; + type Props = { disabled: boolean; docsAvailable?: boolean; @@ -38,6 +41,7 @@ const namedOperations = $derived(operations.filter((o) => o.name !== null)); + /*** HELPER ------------------------------------------- ***/ function onPick(event: Event) { const value = (event.currentTarget as HTMLSelectElement).value; onSelectOperation?.(value || null); @@ -47,41 +51,70 @@ <style lang="scss"> .toolbar { align-items: center; - background: var(--graphiql-panel, #252526); - border-bottom: 1px solid var(--graphiql-border, #333); + background-color: var(--uchu-yang); display: flex; gap: 0.75rem; padding: 0.5rem 0.75rem; } - .run { - background: var(--graphiql-accent, #0e639c); + .run, + .toggle { + align-items: center; border: none; - border-radius: 3px; - color: #fff; - cursor: pointer; - font-size: 0.875rem; - padding: 0.375rem 1rem; + display: flex; + flex-direction: row; + font-size: 0.8rem; + padding: 0.2rem 0.75rem 0.2rem 0.5rem; + + &:not(:disabled) { + cursor: pointer; + } &:disabled { cursor: not-allowed; opacity: 0.5; } + + svg { + width: 1rem; height: 1rem; + + fill: currentColor; + margin-right: 0.25rem; + } + } + + .run { + background-color: var(--uchu-orange-5); + color: var(--uchu-yang); + + &:hover:not(:disabled) { + color: var(--uchu-orange-1); + } } .hint { - color: var(--graphiql-muted, #858585); + color: var(--uchu-yin-5); font-size: 0.75rem; } .picker { - background: var(--graphiql-bg, #1e1e1e); - border: 1px solid var(--graphiql-border, #333); - border-radius: 3px; - color: var(--graphiql-fg, #d4d4d4); - font-family: inherit; - font-size: 0.8125rem; - padding: 0.25rem 0.5rem; + border: none; + border-radius: 0; + font-size: 0.8rem; + padding: 0.2rem 0.75rem 0.2rem 0.5rem; + + &:not(:focus-visible) { + background-color: var(--uchu-gray-1); + color: var(--uchu-yin-5); + } + + &:focus-visible { + background-color: var(--uchu-orange-5); + color: var(--uchu-yang); + outline-color: var(--uchu-orange-5); + outline-offset: -1px; + outline-style: solid; + } } .spacer { @@ -89,35 +122,28 @@ } .toggle { - background: none; - border: 1px solid var(--graphiql-border, #333); - border-radius: 3px; - color: var(--graphiql-muted, #858585); - cursor: pointer; - font-family: inherit; - font-size: 0.75rem; - padding: 0.25rem 0.625rem; + background-color: var(--uchu-gray-1); + color: var(--uchu-yin-5); &.active { - background: var(--graphiql-bg, #1e1e1e); - color: var(--graphiql-fg, #d4d4d4); - } - - &:disabled { - cursor: not-allowed; - opacity: 0.4; + background-color: var(--uchu-yin); + color: var(--uchu-yang); } - &:hover:not(:disabled) { - color: var(--graphiql-fg, #d4d4d4); + &:not(.active):hover:not(:disabled) { + color: var(--uchu-yin-8); } } </style> <div class="toolbar"> <button class="run" {disabled} onclick={onRun}> + <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <path d="M4 3.53711V20.4642L20.927 12.0006L4 3.53711Z"/> + </svg> {running ? "Running…" : "Run"} </button> + {#if namedOperations.length > 1} <select aria-label="Operation" @@ -130,23 +156,43 @@ {/each} </select> {/if} + {#if onFormat} - <button class="toggle" {disabled} onclick={onFormat} type="button">Format</button> + <button class="toggle" {disabled} onclick={onFormat} type="button"> + <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <path d="M20.646 3.36267C20.0461 2.75178 19.7917 1.83922 19.7917 1H19.2083C19.2083 1.83575 18.9563 2.74893 18.3526 3.35264C17.7489 3.95635 16.8358 4.20833 16 4.20833V4.79167C16.8392 4.79167 17.7518 5.04611 18.3627 5.64596C18.9822 6.25433 19.2083 7.15499 19.2083 8H19.7917C19.7917 7.15156 20.0203 6.25159 20.6359 5.63594C21.2516 5.02029 22.1516 4.79167 23 4.79167V4.20833C22.155 4.20833 21.2543 3.98225 20.646 3.36267Z"/> + <path d="M2 13.75C5.03345 13.75 7.04641 14.4196 8.31342 15.6866C9.58043 16.9536 10.25 18.9666 10.25 22H11.75C11.75 18.9666 12.4196 16.9536 13.6866 15.6866C14.9536 14.4196 16.9666 13.75 20 13.75V12.25C16.9666 12.25 14.9536 11.5804 13.6866 10.3134C12.4196 9.04641 11.75 7.03345 11.75 4H10.25C10.25 7.03345 9.58043 9.04641 8.31342 10.3134C7.04641 11.5804 5.03345 12.25 2 12.25V13.75Z"/> + </svg> + Format + </button> {/if} + {#if extras}{@render extras()}{/if} - <span class="hint">⌘/Ctrl + Enter</span> + {#if schemaLoading} <span class="hint">Loading schema…</span> {/if} + <span class="spacer"></span> + {#if onToggleHistory} <button aria-pressed={historyOpen} class="toggle" class:active={historyOpen} onclick={onToggleHistory} - type="button">History</button> + type="button"> + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M7 8C7 8.55228 6.55228 9 6 9C5.44772 9 5 8.55228 5 8C5 7.44772 5.44772 7 6 7C6.55228 7 7 7.44772 7 8Z"/> + <path d="M10 8C10 8.55228 9.55228 9 9 9C8.44772 9 8 8.55228 8 8C8 7.44772 8.44772 7 9 7C9.55228 7 10 7.44772 10 8Z"/> + <path d="M12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9Z"/> + <path d="M2 4V20H11V18.5H3.5V5.5H20.5V9H22V4H2Z"/> + <path d="M13 16.75C14.6446 16.75 15.6575 17.114 16.2718 17.7282C16.886 18.3425 17.25 19.3554 17.25 21H18.75C18.75 19.3554 19.114 18.3425 19.7282 17.7282C20.3425 17.114 21.3554 16.75 23 16.75V15.25C21.3554 15.25 20.3425 14.886 19.7282 14.2718C19.114 13.6575 18.75 12.6446 18.75 11H17.25C17.25 12.6446 16.886 13.6575 16.2718 14.2718C15.6575 14.886 14.6446 15.25 13 15.25V16.75Z"/> + </svg> + History + </button> {/if} + {#if onToggleDocs} <button aria-pressed={docsOpen} @@ -154,6 +200,12 @@ class:active={docsOpen} disabled={!docsAvailable} onclick={onToggleDocs} - type="button">Docs</button> + type="button"> + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M13.5 4V5L16.5 6.5L18 9.5H19L20.5 6.5L21 6.25V21H3V3H15.5L13.5 4ZM7 16.5H14V15H7V16.5ZM7 12.75H10.5V11.25H7V12.75ZM12 12.75H17V11.25H12V12.75ZM7 9H13V7.5H7V9Z"/> + <path d="M18.5 1L19.4453 3.55468L22 4.5L19.4453 5.44532L18.5 8L17.5547 5.44532L15 4.5L17.5547 3.55468L18.5 1Z"/> + </svg> + Docs + </button> {/if} </div> diff --git a/source/library/fetcher/apq.ts b/source/library/fetcher/apq.ts index 7f9f3c1..854017b 100644 --- a/source/library/fetcher/apq.ts +++ b/source/library/fetcher/apq.ts @@ -54,7 +54,7 @@ export function createApqFetcher(options: ApqOptions): Fetcher { }; } -/*** INTERNAL ----------------------------------------- ***/ +/*** HELPER ------------------------------------------- ***/ async function getHash(cache: Map<string, string>, query: string): Promise<string> { const cached = cache.get(query); @@ -64,6 +64,7 @@ async function getHash(cache: Map<string, string>, query: string): Promise<strin const hash = await sha256Hex(query); cache.set(query, hash); + return hash; } diff --git a/source/library/fetcher/sse.ts b/source/library/fetcher/sse.ts index c28436b..73a2da6 100644 --- a/source/library/fetcher/sse.ts +++ b/source/library/fetcher/sse.ts @@ -3,11 +3,19 @@ /*** IMPORT ------------------------------------------- ***/ -import { createClient, type Client, type ClientOptions } from "graphql-sse"; +import { + createClient, + type Client, + type ClientOptions +} from "graphql-sse"; /*** UTILITY ------------------------------------------ ***/ -import type { Fetcher, FetcherOptions, FetcherResult } from "./types.ts"; +import type { + Fetcher, + FetcherOptions, + FetcherResult +} from "./types.ts"; /*** EXPORT ------------------------------------------- ***/ diff --git a/source/library/graphiql.d.ts b/source/library/graphiql.d.ts new file mode 100644 index 0000000..e3c5362 --- /dev/null +++ b/source/library/graphiql.d.ts @@ -0,0 +1 @@ +declare module "@inc/uchu/css"; diff --git a/source/library/graphql/markdown.ts b/source/library/graphql/markdown.ts new file mode 100644 index 0000000..a2ca6f7 --- /dev/null +++ b/source/library/graphql/markdown.ts @@ -0,0 +1,45 @@ + + + +/*** EXPORT ------------------------------------------- ***/ + +export function markdown(input: string): string { + const inlineCode: string[] = []; + const inlineCodePattern = /`([^`]+)`/g; + const refPattern = /\s*\[(\w+)\]\s+<([^>]+)>$/gm; + const refs = new Map<string, string>(); + + let processed = input.replace(inlineCodePattern, (_match, content) => { + const index = inlineCode.length; + inlineCode.push(content); + return `<!--INLINE:CODE:${index}-->`; + }); + + for (const match of processed.matchAll(refPattern)) { + const [, ref, url] = match; + refs.set(ref, url); + } + + processed = processed + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, `<a href="$2" target="_blank">$1</a>`) + .replace(/(\*\*|__)(.*?)\1/g, `<strong style="white-space: nowrap;">$2</strong>`) + .replace(/'/g, "’") + .replace(/(\.\.\.)/g, "…") + .replace(/---/g, "<hr/>"); + + for (const block in inlineCode) { + processed = processed.replace(`<!--INLINE:CODE:${block}-->`, `<code>${escapeHtml(inlineCode[block])}</code>`); + } + + return processed.trimEnd(); +} + +/*** HELPER ------------------------------------------- ***/ + +function escapeHtml(str: string): string { + return str + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """); +} diff --git a/source/library/index.ts b/source/library/index.ts index d68955b..5f18a87 100644 --- a/source/library/index.ts +++ b/source/library/index.ts @@ -3,6 +3,8 @@ /*** EXPORT ------------------------------------------- ***/ +export type { Extension } from "@codemirror/state"; + export { createApqFetcher } from "./fetcher/apq.ts"; export { createHttpFetcher } from "./fetcher/http.ts"; export { createLocalStorage, createMemoryStorage } from "./state/storage.ts"; @@ -17,7 +19,6 @@ export { SessionStore } from "./state/session.svelte.ts"; export { validateSessionExport } from "./state/session-io.ts"; export type { ApqOptions } from "./fetcher/apq.ts"; -export type { Extension } from "@codemirror/state"; export type { Fetcher, @@ -27,15 +28,18 @@ export type { } from "./fetcher/types.ts"; export type { HistoryEntry, HistoryInput } from "./state/history.svelte.ts"; + export type { ImportResult, SessionExport, TabExport } from "./state/session-io.ts"; + export type { OperationInfo } from "./graphql/operations.ts"; export type { ShortcutAction } from "./state/keyboard.ts"; export type { SseFetcherOptions } from "./fetcher/sse.ts"; export type { Storage } from "./state/storage.ts"; + export type { RunOptions, SubscriptionMode, @@ -43,4 +47,5 @@ export type { TabSeed, TabTiming } from "./state/session.svelte.ts"; + export type { WsFetcherOptions } from "./fetcher/websocket.ts"; diff --git a/source/library/runes.d.ts b/source/library/runes.d.ts deleted file mode 100644 index 4b73482..0000000 --- a/source/library/runes.d.ts +++ /dev/null @@ -1,37 +0,0 @@ - - - -/*** EXPORT ------------------------------------------- ***/ - -/** - * Ambient declarations for Svelte 5 runes so `deno check` can type-check - * `.svelte.ts` files. The runtime forms are injected by the Svelte compiler. - */ - -declare function $state<T>(initial: T): T; -declare function $state<T>(): T | undefined; - -declare namespace $state { - function raw<T>(initial: T): T; - function raw<T>(): T | undefined; - function snapshot<T>(value: T): T; -} - -declare function $derived<T>(expression: T): T; - -declare namespace $derived { - function by<T>(fn: () => T): T; -} - -declare function $effect(fn: () => void | (() => void)): void; - -declare namespace $effect { - function pre(fn: () => void | (() => void)): void; - function root(fn: () => void | (() => void)): () => void; - function tracking(): boolean; -} - -declare function $props<T = Record<string, unknown>>(): T; -declare function $bindable<T>(fallback?: T): T; -declare function $inspect<T>(...values: T[]): { with: (fn: (type: "init" | "update", ...values: T[]) => void) => void }; -declare function $host<T extends HTMLElement = HTMLElement>(): T; diff --git a/source/library/state/history.svelte.ts b/source/library/state/history.svelte.ts index 2726283..80de3c0 100644 --- a/source/library/state/history.svelte.ts +++ b/source/library/state/history.svelte.ts @@ -32,12 +32,10 @@ export type HistoryInput = { export class HistoryStore { entries = $state<HistoryEntry[]>([]); - #storage: Storage; constructor(storage: Storage) { this.#storage = storage; - const restored = storage.get<HistoryEntry[]>(STORAGE_KEY); if (Array.isArray(restored)) diff --git a/source/library/state/session-io.ts b/source/library/state/session-io.ts index a5e2ea9..f2491ba 100644 --- a/source/library/state/session-io.ts +++ b/source/library/state/session-io.ts @@ -9,59 +9,14 @@ import type { Tab } from "./session.svelte.ts"; const MAX_STRING_BYTES = 1024 * 1024; const MAX_TABS = 50; -function isObject(value: unknown): value is Record<string, unknown> { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function stringTooLong(value: string): boolean { - return value.length > MAX_STRING_BYTES; -} - -function validateTab(raw: unknown, index: number): TabExport | string { - if (!isObject(raw)) - return `tabs[${index}]: not an object`; - - if (typeof raw.headers !== "string") - return `tabs[${index}].headers: not a string`; - - if (typeof raw.query !== "string") - return `tabs[${index}].query: not a string`; - - if (typeof raw.title !== "string") - return `tabs[${index}].title: not a string`; - - if (typeof raw.variables !== "string") - return `tabs[${index}].variables: not a string`; - - if (raw.operationName !== null && typeof raw.operationName !== "string") - return `tabs[${index}].operationName: not a string or null`; - - if (stringTooLong(raw.headers)) - return `tabs[${index}].headers: exceeds 1 MB`; - - if (stringTooLong(raw.query)) - return `tabs[${index}].query: exceeds 1 MB`; - - if (stringTooLong(raw.title)) - return `tabs[${index}].title: exceeds 1 MB`; - - if (stringTooLong(raw.variables)) - return `tabs[${index}].variables: exceeds 1 MB`; - - if (typeof raw.operationName === "string" && stringTooLong(raw.operationName)) - return `tabs[${index}].operationName: exceeds 1 MB`; - - return { - headers: raw.headers, - operationName: raw.operationName, - query: raw.query, - title: raw.title, - variables: raw.variables - }; -} - /*** EXPORT ------------------------------------------- ***/ +export type ImportResult = { + added: number; + errors: string[]; + skipped: number; +}; + export type TabExport = { headers: string; operationName: string | null; @@ -76,12 +31,6 @@ export type SessionExport = { version: 1; }; -export type ImportResult = { - added: number; - errors: string[]; - skipped: number; -}; - export function tabToExport(tab: Tab): TabExport { return { headers: tab.headers, @@ -125,3 +74,56 @@ export function validateSessionExport(data: unknown): SessionExport | { error: s version: 1 }; } + +/*** HELPER ------------------------------------------- ***/ + +function isObject(value: unknown): value is Record<string, unknown> { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function stringTooLong(value: string): boolean { + return value.length > MAX_STRING_BYTES; +} + +function validateTab(raw: unknown, index: number): TabExport | string { + if (!isObject(raw)) + return `tabs[${index}]: not an object`; + + if (typeof raw.headers !== "string") + return `tabs[${index}].headers: not a string`; + + if (typeof raw.query !== "string") + return `tabs[${index}].query: not a string`; + + if (typeof raw.title !== "string") + return `tabs[${index}].title: not a string`; + + if (typeof raw.variables !== "string") + return `tabs[${index}].variables: not a string`; + + if (raw.operationName !== null && typeof raw.operationName !== "string") + return `tabs[${index}].operationName: not a string or null`; + + if (stringTooLong(raw.headers)) + return `tabs[${index}].headers: exceeds 1 MB`; + + if (stringTooLong(raw.query)) + return `tabs[${index}].query: exceeds 1 MB`; + + if (stringTooLong(raw.title)) + return `tabs[${index}].title: exceeds 1 MB`; + + if (stringTooLong(raw.variables)) + return `tabs[${index}].variables: exceeds 1 MB`; + + if (typeof raw.operationName === "string" && stringTooLong(raw.operationName)) + return `tabs[${index}].operationName: exceeds 1 MB`; + + return { + headers: raw.headers, + operationName: raw.operationName, + query: raw.query, + title: raw.title, + variables: raw.variables + }; +} diff --git a/source/library/state/session.svelte.ts b/source/library/state/session.svelte.ts index 76777e5..f84bb17 100644 --- a/source/library/state/session.svelte.ts +++ b/source/library/state/session.svelte.ts @@ -3,29 +3,26 @@ /*** UTILITY ------------------------------------------ ***/ -import type { Fetcher, FetcherResult } from "../fetcher/types.ts"; import { format } from "../graphql/format.ts"; + import { deriveTitle, parseOperations, type OperationInfo } from "../graphql/operations.ts"; + import { tabToExport, type ImportResult, type SessionExport, type TabExport } from "./session-io.ts"; + +import type { Fetcher, FetcherResult } from "../fetcher/types.ts"; import type { Storage } from "./storage.ts"; const STORAGE_KEY = "session"; -function isAsyncIterable<T>(value: unknown): value is AsyncIterable<T> { - return typeof value === "object" && - value !== null && - typeof (value as AsyncIterable<T>)[Symbol.asyncIterator] === "function"; -} - type Snapshot = { activeId: string; tabs: Tab[]; @@ -72,7 +69,6 @@ export class SessionStore { activeId = $state<string>(""); tabs = $state<Tab[]>([]); active = $derived(this.tabs.find((t) => t.id === this.activeId)); - #storage: Storage; constructor(storage: Storage) { @@ -106,6 +102,7 @@ export class SessionStore { if (this.tabs.length === 1) { const fresh = this.#blank(); + this.tabs = [fresh]; this.activeId = fresh.id; @@ -154,8 +151,8 @@ export class SessionStore { } importTabs(data: SessionExport, opts: { mode: "append" | "replace" }): ImportResult { - const errors: string[] = []; const capped = data.tabs.slice(0, 50); + const errors: string[] = []; const skipped = data.tabs.length - capped.length; if (opts.mode === "replace") { @@ -181,7 +178,11 @@ export class SessionStore { if (capped.length > 0) this.activeId = this.tabs[this.tabs.length - 1].id; - return { added: capped.length, errors, skipped }; + return { + added: capped.length, + errors, + skipped + }; } nextTab() { @@ -249,8 +250,8 @@ export class SessionStore { tab.timing = { endMs: startMs, firstByteMs: startMs, startMs }; try { - const variables = tab.variables.trim() ? JSON.parse(tab.variables) : {}; const headers = tab.headers.trim() ? JSON.parse(tab.headers) : {}; + const variables = tab.variables.trim() ? JSON.parse(tab.variables) : {}; const result = await fetcher({ headers, @@ -312,13 +313,16 @@ export class SessionStore { } const firstByteMs = performance.now(); + tab.timing = { ...tab.timing, firstByteMs }; tab.result = JSON.stringify(result, null, 2); tab.timing = { ...tab.timing, endMs: performance.now() }; + return true; } catch(err) { tab.result = JSON.stringify({ error: String(err) }, null, 2); tab.timing = { ...tab.timing, endMs: performance.now() }; + return false; } } @@ -431,3 +435,11 @@ export class SessionStore { }; } } + +/*** HELPER ------------------------------------------- ***/ + +function isAsyncIterable<T>(value: unknown): value is AsyncIterable<T> { + return typeof value === "object" && + value !== null && + typeof (value as AsyncIterable<T>)[Symbol.asyncIterator] === "function"; +} diff --git a/source/library/styles/theme.scss b/source/library/styles/theme.scss deleted file mode 100644 index 55f24aa..0000000 --- a/source/library/styles/theme.scss +++ /dev/null @@ -1,47 +0,0 @@ -// @eol/graphiql chrome theme variables. -// -// Override any variable under a scoped selector (e.g. `.graphiql`) or on :root -// to re-skin the chrome. The editor theme is a separate prop (`theme?: Extension`). -// -// Supported variables: -// --graphiql-accent primary action color (Run button, selected state) -// --graphiql-bg base background (editor + result) -// --graphiql-border divider lines between panes -// --graphiql-fg primary foreground -// --graphiql-font font-family for the chrome -// --graphiql-link clickable links in docs / breadcrumbs -// --graphiql-muted muted foreground (hints, timestamps) -// --graphiql-panel panel background (toolbar, tabbar, headers) - -:root { - --graphiql-accent: #0e639c; - --graphiql-bg: #1e1e1e; - --graphiql-border: #333; - --graphiql-fg: #d4d4d4; - --graphiql-font: ui-monospace, SFMono-Regular, monospace; - --graphiql-link: #79b8ff; - --graphiql-muted: #858585; - --graphiql-panel: #252526; -} - -@media (prefers-color-scheme: light) { - :root:not(.graphiql-dark) { - --graphiql-accent: #0366d6; - --graphiql-bg: #ffffff; - --graphiql-border: #e1e4e8; - --graphiql-fg: #24292e; - --graphiql-link: #0366d6; - --graphiql-muted: #6a737d; - --graphiql-panel: #f6f8fa; - } -} - -:root.graphiql-light { - --graphiql-accent: #0366d6; - --graphiql-bg: #ffffff; - --graphiql-border: #e1e4e8; - --graphiql-fg: #24292e; - --graphiql-link: #0366d6; - --graphiql-muted: #6a737d; - --graphiql-panel: #f6f8fa; -} diff --git a/source/library/themes/light.ts b/source/library/themes/light.ts index daaede2..3e20423 100644 --- a/source/library/themes/light.ts +++ b/source/library/themes/light.ts @@ -3,18 +3,19 @@ /*** IMPORT ------------------------------------------- ***/ +import { color, yang } from "@inc/uchu"; +import { Decoration, drawSelection, EditorView, ViewPlugin, WidgetType } from "@codemirror/view"; import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; -import { EditorView } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; -/*** EXPORT ------------------------------------------- ***/ +/*** UTILITY ------------------------------------------ ***/ -const BG = "#fafafa"; -const BORDER = "#e0e0e0"; -const FG = "#24292e"; -const GUTTER_BG = "#f3f3f3"; -const GUTTER_FG = "#9ca3af"; -const SELECTION = "#b3d4fc"; +const BG = yang; +const BORDER = color("gray", 1); +const FG = color("yin", 3); +const GUTTER_BG = color("gray", 1); +const GUTTER_FG = color("gray", 6); +const SELECTION = color("yellow", 1); const base = EditorView.theme({ "&": { @@ -24,18 +25,21 @@ const base = EditorView.theme({ "&.cm-focused": { outline: "none" }, + "&.cm-focused .cm-matchingBracket": { + backgroundColor: color("gray", 1) + }, + "&.cm-focused .cm-selectionBackground, ::selection": { + backgroundColor: SELECTION + }, ".cm-activeLine": { - backgroundColor: "#f0f0f0" + backgroundColor: color("gray", 1) }, ".cm-activeLineGutter": { backgroundColor: "transparent", color: FG }, ".cm-content": { - caretColor: "#1f6feb" - }, - ".cm-cursor, .cm-dropCursor": { - borderLeftColor: "#1f6feb" + padding: 0 }, ".cm-gutters": { backgroundColor: GUTTER_BG, @@ -43,33 +47,101 @@ const base = EditorView.theme({ borderRight: `1px solid ${BORDER}`, color: GUTTER_FG }, + ".cm-lineNumbers .cm-gutterElement": { + padding: "0 5px 0 15px" + }, + ".cm-lintPoint::after": { + borderBottomColor: color("red", 5) + }, ".cm-matchingBracket": { - backgroundColor: "#dbeafe", - outline: "1px solid #93c5fd" + backgroundColor: color("gray", 1), + color: color("yin", 8), + outline: "none" + }, + ".cm-scroller": { + fontSize: "0.8rem", + lineHeight: 1.3 }, ".cm-selectionBackground, &.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground": { backgroundColor: SELECTION } }, { dark: false }); +const blockCaretTheme = EditorView.theme({ + "&": { + caretColor: "transparent" + }, + "& .cm-cursor": { + borderLeftColor: color("blue", 5), + borderLeftStyle: "solid", + borderLeftWidth: "1ch", + marginLeft: "0" + }, + "& .cm-cursorLayer": { + mixBlendMode: "hard-light", + zIndex: "auto" + }, + "& .cm-scroller": { + isolation: "isolate" + } +}); + const highlight = HighlightStyle.define([ - { color: "#d73a49", tag: t.keyword }, - { color: "#6f42c1", tag: [t.name, t.deleted, t.character, t.macroName] }, - { color: "#6f42c1", tag: [t.propertyName] }, - { color: "#032f62", tag: [t.string, t.special(t.string)] }, - { color: "#005cc5", tag: [t.number, t.bool, t.null, t.atom] }, - { color: "#6a737d", fontStyle: "italic", tag: t.comment }, - { color: "#22863a", tag: [t.typeName, t.className] }, - { color: "#e36209", tag: [t.variableName, t.labelName] }, - { color: "#d73a49", tag: [t.operator, t.operatorKeyword] }, - { color: "#6a737d", tag: [t.meta, t.documentMeta] }, - { color: "#22863a", tag: [t.tagName] }, - { color: "#6f42c1", tag: [t.attributeName] }, - { color: "#e36209", tag: [t.heading] }, - { color: "#032f62", tag: [t.link] }, - { fontWeight: "bold", tag: [t.strong] }, - { fontStyle: "italic", tag: [t.emphasis] }, - { tag: t.strikethrough, textDecoration: "line-through" } + { + color: color("yin", 8), + tag: [ + t.atom, + t.attributeName, + t.bool, + t.character, + t.className, + t.comment, + t.deleted, + t.documentMeta, + t.keyword, + t.heading, + t.labelName, + t.link, + t.macroName, + t.meta, + t.name, + t.null, + t.operator, + t.operatorKeyword, + t.propertyName, + t.tagName, + t.typeName, + t.variableName + ] + }, + { + backgroundColor: color("green", 1), + color: color("green", 9), + tag: [t.string, t.special(t.string)] + }, + { + color: color("purple", 4), + tag: [t.number] + }, + { + fontStyle: "italic", + tag: [t.comment, t.emphasis] + }, + { + fontWeight: "bold", + tag: [t.strong] + }, + { + tag: t.strikethrough, + textDecoration: "line-through" + } ]); -export const lightTheme = [base, syntaxHighlighting(highlight)]; +/*** EXPORT ------------------------------------------- ***/ + +export const lightTheme = [ + drawSelection(), + base, + syntaxHighlighting(highlight), + blockCaretTheme +]; |