@brainfish-ai/devdoc 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +415 -0
  3. package/bin/devdoc.js +13 -0
  4. package/dist/cli/commands/build.d.ts +5 -0
  5. package/dist/cli/commands/build.js +87 -0
  6. package/dist/cli/commands/check.d.ts +1 -0
  7. package/dist/cli/commands/check.js +143 -0
  8. package/dist/cli/commands/create.d.ts +24 -0
  9. package/dist/cli/commands/create.js +387 -0
  10. package/dist/cli/commands/deploy.d.ts +9 -0
  11. package/dist/cli/commands/deploy.js +433 -0
  12. package/dist/cli/commands/dev.d.ts +6 -0
  13. package/dist/cli/commands/dev.js +139 -0
  14. package/dist/cli/commands/init.d.ts +11 -0
  15. package/dist/cli/commands/init.js +238 -0
  16. package/dist/cli/commands/keys.d.ts +12 -0
  17. package/dist/cli/commands/keys.js +165 -0
  18. package/dist/cli/commands/start.d.ts +5 -0
  19. package/dist/cli/commands/start.js +56 -0
  20. package/dist/cli/commands/upload.d.ts +13 -0
  21. package/dist/cli/commands/upload.js +238 -0
  22. package/dist/cli/commands/whoami.d.ts +8 -0
  23. package/dist/cli/commands/whoami.js +91 -0
  24. package/dist/cli/index.d.ts +1 -0
  25. package/dist/cli/index.js +106 -0
  26. package/dist/config/index.d.ts +80 -0
  27. package/dist/config/index.js +133 -0
  28. package/dist/constants.d.ts +9 -0
  29. package/dist/constants.js +13 -0
  30. package/dist/index.d.ts +7 -0
  31. package/dist/index.js +12 -0
  32. package/dist/utils/logger.d.ts +16 -0
  33. package/dist/utils/logger.js +61 -0
  34. package/dist/utils/paths.d.ts +16 -0
  35. package/dist/utils/paths.js +50 -0
  36. package/package.json +51 -0
  37. package/renderer/app/api/assets/[...path]/route.ts +123 -0
  38. package/renderer/app/api/assets/route.ts +124 -0
  39. package/renderer/app/api/assets/upload/route.ts +177 -0
  40. package/renderer/app/api/auth-schemes/route.ts +77 -0
  41. package/renderer/app/api/chat/route.ts +858 -0
  42. package/renderer/app/api/codegen/route.ts +72 -0
  43. package/renderer/app/api/collections/route.ts +1016 -0
  44. package/renderer/app/api/debug/route.ts +53 -0
  45. package/renderer/app/api/deploy/route.ts +234 -0
  46. package/renderer/app/api/device/route.ts +42 -0
  47. package/renderer/app/api/docs/route.ts +187 -0
  48. package/renderer/app/api/keys/regenerate/route.ts +80 -0
  49. package/renderer/app/api/openapi-spec/route.ts +151 -0
  50. package/renderer/app/api/projects/[slug]/route.ts +153 -0
  51. package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
  52. package/renderer/app/api/projects/register/route.ts +152 -0
  53. package/renderer/app/api/proxy/route.ts +149 -0
  54. package/renderer/app/api/proxy-stream/route.ts +168 -0
  55. package/renderer/app/api/redirects/route.ts +47 -0
  56. package/renderer/app/api/schema/route.ts +65 -0
  57. package/renderer/app/api/subdomains/check/route.ts +172 -0
  58. package/renderer/app/api/suggestions/route.ts +144 -0
  59. package/renderer/app/favicon.ico +0 -0
  60. package/renderer/app/globals.css +1103 -0
  61. package/renderer/app/layout.tsx +47 -0
  62. package/renderer/app/llms-full.txt/route.ts +346 -0
  63. package/renderer/app/llms.txt/route.ts +279 -0
  64. package/renderer/app/page.tsx +14 -0
  65. package/renderer/app/robots.txt/route.ts +84 -0
  66. package/renderer/app/sitemap.xml/route.ts +199 -0
  67. package/renderer/components/docs/index.ts +12 -0
  68. package/renderer/components/docs/mdx/accordion.tsx +169 -0
  69. package/renderer/components/docs/mdx/badge.tsx +132 -0
  70. package/renderer/components/docs/mdx/callouts.tsx +154 -0
  71. package/renderer/components/docs/mdx/cards.tsx +213 -0
  72. package/renderer/components/docs/mdx/changelog.tsx +120 -0
  73. package/renderer/components/docs/mdx/code-block.tsx +186 -0
  74. package/renderer/components/docs/mdx/code-group.tsx +421 -0
  75. package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
  76. package/renderer/components/docs/mdx/frame.tsx +112 -0
  77. package/renderer/components/docs/mdx/highlight.tsx +151 -0
  78. package/renderer/components/docs/mdx/iframe.tsx +134 -0
  79. package/renderer/components/docs/mdx/image.tsx +235 -0
  80. package/renderer/components/docs/mdx/index.ts +204 -0
  81. package/renderer/components/docs/mdx/mermaid.tsx +240 -0
  82. package/renderer/components/docs/mdx/param-field.tsx +200 -0
  83. package/renderer/components/docs/mdx/steps.tsx +113 -0
  84. package/renderer/components/docs/mdx/tabs.tsx +86 -0
  85. package/renderer/components/docs/mdx-renderer.tsx +100 -0
  86. package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
  87. package/renderer/components/docs/navigation/index.ts +8 -0
  88. package/renderer/components/docs/navigation/page-nav.tsx +64 -0
  89. package/renderer/components/docs/navigation/sidebar.tsx +515 -0
  90. package/renderer/components/docs/navigation/toc.tsx +113 -0
  91. package/renderer/components/docs/notice.tsx +105 -0
  92. package/renderer/components/docs-header.tsx +274 -0
  93. package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
  94. package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
  95. package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
  96. package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
  97. package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
  98. package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
  99. package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
  100. package/renderer/components/docs-viewer/agent/index.tsx +6 -0
  101. package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
  102. package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
  103. package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
  104. package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
  105. package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
  106. package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
  107. package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
  108. package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
  109. package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
  110. package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
  111. package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
  112. package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
  113. package/renderer/components/docs-viewer/content/index.tsx +29 -0
  114. package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
  115. package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
  116. package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
  117. package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
  118. package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
  119. package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
  120. package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
  121. package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
  122. package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
  123. package/renderer/components/docs-viewer/index.tsx +1466 -0
  124. package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
  125. package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
  126. package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
  127. package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
  128. package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
  129. package/renderer/components/docs-viewer/playground/index.tsx +671 -0
  130. package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
  131. package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
  132. package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
  133. package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
  134. package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
  135. package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
  136. package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
  137. package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
  138. package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
  139. package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
  140. package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
  141. package/renderer/components/docs-viewer/search/index.ts +2 -0
  142. package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
  143. package/renderer/components/docs-viewer/search/use-search.ts +117 -0
  144. package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
  145. package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
  146. package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
  147. package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
  148. package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
  149. package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
  150. package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
  151. package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
  152. package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
  153. package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
  154. package/renderer/components/theme-provider.tsx +11 -0
  155. package/renderer/components/theme-toggle.tsx +76 -0
  156. package/renderer/components/ui/badge.tsx +46 -0
  157. package/renderer/components/ui/button.tsx +59 -0
  158. package/renderer/components/ui/dialog.tsx +118 -0
  159. package/renderer/components/ui/dropdown-menu.tsx +257 -0
  160. package/renderer/components/ui/input.tsx +21 -0
  161. package/renderer/components/ui/label.tsx +24 -0
  162. package/renderer/components/ui/navigation-menu.tsx +168 -0
  163. package/renderer/components/ui/select.tsx +190 -0
  164. package/renderer/components/ui/spinner.tsx +114 -0
  165. package/renderer/components/ui/tabs.tsx +66 -0
  166. package/renderer/components/ui/tooltip.tsx +61 -0
  167. package/renderer/hooks/use-code-copy.ts +88 -0
  168. package/renderer/hooks/use-openapi-title.ts +44 -0
  169. package/renderer/lib/api-docs/agent/index.ts +6 -0
  170. package/renderer/lib/api-docs/agent/indexer.ts +323 -0
  171. package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
  172. package/renderer/lib/api-docs/agent/types.ts +116 -0
  173. package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
  174. package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
  175. package/renderer/lib/api-docs/auth/crypto.ts +89 -0
  176. package/renderer/lib/api-docs/auth/index.ts +4 -0
  177. package/renderer/lib/api-docs/code-editor/db.ts +164 -0
  178. package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
  179. package/renderer/lib/api-docs/code-editor/index.ts +6 -0
  180. package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
  181. package/renderer/lib/api-docs/code-editor/types.ts +105 -0
  182. package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
  183. package/renderer/lib/api-docs/codegen/har.ts +251 -0
  184. package/renderer/lib/api-docs/codegen/index.ts +159 -0
  185. package/renderer/lib/api-docs/factories.ts +151 -0
  186. package/renderer/lib/api-docs/index.ts +17 -0
  187. package/renderer/lib/api-docs/mobile-context.tsx +112 -0
  188. package/renderer/lib/api-docs/navigation-context.tsx +88 -0
  189. package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
  190. package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
  191. package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
  192. package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
  193. package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
  194. package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
  195. package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
  196. package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
  197. package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
  198. package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
  199. package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
  200. package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
  201. package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
  202. package/renderer/lib/api-docs/playground/context.tsx +107 -0
  203. package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
  204. package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
  205. package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
  206. package/renderer/lib/api-docs/playground/types.ts +35 -0
  207. package/renderer/lib/api-docs/types.ts +269 -0
  208. package/renderer/lib/api-docs/utils.ts +311 -0
  209. package/renderer/lib/cache.ts +193 -0
  210. package/renderer/lib/docs/config/index.ts +29 -0
  211. package/renderer/lib/docs/config/loader.ts +142 -0
  212. package/renderer/lib/docs/config/schema.ts +298 -0
  213. package/renderer/lib/docs/index.ts +12 -0
  214. package/renderer/lib/docs/mdx/compiler.ts +176 -0
  215. package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
  216. package/renderer/lib/docs/mdx/index.ts +26 -0
  217. package/renderer/lib/docs/navigation/generator.ts +348 -0
  218. package/renderer/lib/docs/navigation/index.ts +12 -0
  219. package/renderer/lib/docs/navigation/types.ts +123 -0
  220. package/renderer/lib/docs-navigation-context.tsx +80 -0
  221. package/renderer/lib/multi-tenant/context.ts +105 -0
  222. package/renderer/lib/storage/blob.ts +845 -0
  223. package/renderer/lib/utils.ts +6 -0
  224. package/renderer/next.config.ts +76 -0
  225. package/renderer/package.json +66 -0
  226. package/renderer/postcss.config.mjs +5 -0
  227. package/renderer/public/assets/images/screenshot.png +0 -0
  228. package/renderer/public/assets/logo/dark.svg +9 -0
  229. package/renderer/public/assets/logo/light.svg +9 -0
  230. package/renderer/public/assets/logo.svg +9 -0
  231. package/renderer/public/file.svg +1 -0
  232. package/renderer/public/globe.svg +1 -0
  233. package/renderer/public/icon.png +0 -0
  234. package/renderer/public/logo.svg +9 -0
  235. package/renderer/public/window.svg +1 -0
  236. package/renderer/tsconfig.json +28 -0
  237. package/templates/basic/README.md +139 -0
  238. package/templates/basic/assets/favicon.svg +4 -0
  239. package/templates/basic/assets/logo.svg +9 -0
  240. package/templates/basic/docs.json +47 -0
  241. package/templates/basic/guides/configuration.mdx +149 -0
  242. package/templates/basic/guides/overview.mdx +96 -0
  243. package/templates/basic/index.mdx +39 -0
  244. package/templates/basic/package.json +14 -0
  245. package/templates/basic/quickstart.mdx +92 -0
  246. package/templates/basic/vercel.json +6 -0
  247. package/templates/graphql/README.md +139 -0
  248. package/templates/graphql/api-reference/schema.graphql +305 -0
  249. package/templates/graphql/assets/favicon.svg +4 -0
  250. package/templates/graphql/assets/logo.svg +9 -0
  251. package/templates/graphql/docs.json +54 -0
  252. package/templates/graphql/guides/configuration.mdx +149 -0
  253. package/templates/graphql/guides/overview.mdx +96 -0
  254. package/templates/graphql/index.mdx +39 -0
  255. package/templates/graphql/package.json +14 -0
  256. package/templates/graphql/quickstart.mdx +92 -0
  257. package/templates/graphql/vercel.json +6 -0
  258. package/templates/openapi/README.md +139 -0
  259. package/templates/openapi/api-reference/openapi.json +419 -0
  260. package/templates/openapi/assets/favicon.svg +4 -0
  261. package/templates/openapi/assets/logo.svg +9 -0
  262. package/templates/openapi/docs.json +61 -0
  263. package/templates/openapi/guides/configuration.mdx +149 -0
  264. package/templates/openapi/guides/overview.mdx +96 -0
  265. package/templates/openapi/index.mdx +39 -0
  266. package/templates/openapi/package.json +14 -0
  267. package/templates/openapi/quickstart.mdx +92 -0
  268. package/templates/openapi/vercel.json +6 -0
@@ -0,0 +1,745 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback, useEffect, useRef } from 'react'
4
+ import { cn } from '@/lib/utils'
5
+ import {
6
+ Play,
7
+ ArrowClockwise,
8
+ Copy,
9
+ Check,
10
+ Spinner,
11
+ X,
12
+ FileText,
13
+ BracketsCurly,
14
+ Key,
15
+ CaretRight,
16
+ CaretDown,
17
+ MagnifyingGlass,
18
+ Lightning,
19
+ ArrowsClockwise
20
+ } from '@phosphor-icons/react'
21
+ import { Button } from '@/components/ui/button'
22
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
23
+ import Editor, { loader } from '@monaco-editor/react'
24
+
25
+ // Configure Monaco to use CDN
26
+ loader.config({
27
+ paths: {
28
+ vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs',
29
+ },
30
+ })
31
+
32
+ // ============================================================================
33
+ // Types
34
+ // ============================================================================
35
+
36
+ export interface GraphQLOperationItem {
37
+ id: string
38
+ name: string
39
+ description?: string | null
40
+ operationType: 'query' | 'mutation' | 'subscription'
41
+ query: string
42
+ exampleVariables?: Record<string, unknown>
43
+ }
44
+
45
+ interface GraphQLPlaygroundProps {
46
+ /** GraphQL endpoint URL */
47
+ endpoint: string
48
+ /** Initial query to display */
49
+ defaultQuery?: string
50
+ /** Headers to include with requests */
51
+ headers?: Record<string, string>
52
+ /** Operations from parsed schema */
53
+ operations?: GraphQLOperationItem[]
54
+ /** Selected operation ID (controlled from outside) */
55
+ selectedOperationId?: string
56
+ /** Hide internal schema explorer (when using main sidebar) */
57
+ hideExplorer?: boolean
58
+ /** Custom class name */
59
+ className?: string
60
+ /** Theme */
61
+ theme?: 'light' | 'dark'
62
+ }
63
+
64
+ interface GraphQLResponse {
65
+ data?: unknown
66
+ errors?: Array<{
67
+ message: string
68
+ locations?: Array<{ line: number; column: number }>
69
+ path?: Array<string | number>
70
+ }>
71
+ extensions?: Record<string, unknown>
72
+ }
73
+
74
+ type TabType = 'variables' | 'headers'
75
+
76
+ const STORAGE_KEY = 'brainfish-graphql-playground'
77
+
78
+ // ============================================================================
79
+ // Helpers
80
+ // ============================================================================
81
+
82
+ function loadStoredState(): { query?: string; variables?: string; headers?: string } | null {
83
+ if (typeof window === 'undefined') return null
84
+ try {
85
+ const stored = localStorage.getItem(STORAGE_KEY)
86
+ if (stored) return JSON.parse(stored)
87
+ } catch { /* ignore */ }
88
+ return null
89
+ }
90
+
91
+ function saveStoredState(state: { query: string; variables: string; headers: string }): void {
92
+ if (typeof window === 'undefined') return
93
+ try {
94
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
95
+ } catch { /* ignore */ }
96
+ }
97
+
98
+ // ============================================================================
99
+ // Schema Explorer Sidebar
100
+ // ============================================================================
101
+
102
+ interface SchemaExplorerProps {
103
+ operations: GraphQLOperationItem[]
104
+ onSelectOperation: (operation: GraphQLOperationItem) => void
105
+ selectedOperationId?: string
106
+ }
107
+
108
+ function SchemaExplorer({ operations, onSelectOperation, selectedOperationId }: SchemaExplorerProps) {
109
+ const [searchQuery, setSearchQuery] = useState('')
110
+ const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
111
+ query: true,
112
+ mutation: true,
113
+ subscription: true,
114
+ })
115
+
116
+ // Group operations by type
117
+ const queries = operations.filter(op => op.operationType === 'query')
118
+ const mutations = operations.filter(op => op.operationType === 'mutation')
119
+ const subscriptions = operations.filter(op => op.operationType === 'subscription')
120
+
121
+ // Filter by search
122
+ const filterOps = (ops: GraphQLOperationItem[]) => {
123
+ if (!searchQuery.trim()) return ops
124
+ const query = searchQuery.toLowerCase()
125
+ return ops.filter(op =>
126
+ op.name.toLowerCase().includes(query) ||
127
+ op.description?.toLowerCase().includes(query)
128
+ )
129
+ }
130
+
131
+ const filteredQueries = filterOps(queries)
132
+ const filteredMutations = filterOps(mutations)
133
+ const filteredSubscriptions = filterOps(subscriptions)
134
+
135
+ const toggleSection = (section: string) => {
136
+ setExpandedSections(prev => ({
137
+ ...prev,
138
+ [section]: !prev[section],
139
+ }))
140
+ }
141
+
142
+ const renderSection = (
143
+ title: string,
144
+ icon: React.ReactNode,
145
+ ops: GraphQLOperationItem[],
146
+ sectionKey: string,
147
+ iconColor: string
148
+ ) => {
149
+ if (ops.length === 0) return null
150
+ const isExpanded = expandedSections[sectionKey]
151
+
152
+ return (
153
+ <div className="mb-2">
154
+ <button
155
+ onClick={() => toggleSection(sectionKey)}
156
+ className="w-full flex items-center gap-2 px-3 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground hover:bg-muted/50 transition-colors"
157
+ >
158
+ {isExpanded ? (
159
+ <CaretDown className="w-3 h-3" />
160
+ ) : (
161
+ <CaretRight className="w-3 h-3" />
162
+ )}
163
+ <span className={iconColor}>{icon}</span>
164
+ <span>{title}</span>
165
+ <span className="ml-auto text-[10px] font-normal opacity-60">
166
+ {ops.length}
167
+ </span>
168
+ </button>
169
+
170
+ {isExpanded && (
171
+ <div className="ml-2">
172
+ {ops.map((op) => (
173
+ <button
174
+ key={op.id}
175
+ onClick={() => onSelectOperation(op)}
176
+ className={cn(
177
+ 'w-full flex items-start gap-2 px-3 py-2 text-left',
178
+ 'hover:bg-muted/50 transition-colors rounded-sm',
179
+ 'border-l-2',
180
+ selectedOperationId === op.id
181
+ ? 'bg-muted/70 border-primary'
182
+ : 'border-transparent'
183
+ )}
184
+ >
185
+ <div className="flex-1 min-w-0">
186
+ <div className="text-sm font-medium truncate">
187
+ {op.name}
188
+ </div>
189
+ {op.description && (
190
+ <div className="text-xs text-muted-foreground truncate mt-0.5">
191
+ {op.description}
192
+ </div>
193
+ )}
194
+ </div>
195
+ </button>
196
+ ))}
197
+ </div>
198
+ )}
199
+ </div>
200
+ )
201
+ }
202
+
203
+ const hasOperations = operations.length > 0
204
+ const hasFilteredResults = filteredQueries.length > 0 || filteredMutations.length > 0 || filteredSubscriptions.length > 0
205
+
206
+ return (
207
+ <div className="flex flex-col h-full border-r border-border bg-muted/20">
208
+ {/* Search */}
209
+ <div className="p-2 border-b border-border">
210
+ <div className="relative">
211
+ <MagnifyingGlass className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
212
+ <input
213
+ type="text"
214
+ value={searchQuery}
215
+ onChange={(e) => setSearchQuery(e.target.value)}
216
+ placeholder="Search operations..."
217
+ className={cn(
218
+ 'w-full pl-8 pr-3 py-1.5 text-sm rounded-md',
219
+ 'bg-background border border-border',
220
+ 'focus:outline-none focus:ring-1 focus:ring-primary',
221
+ 'placeholder:text-muted-foreground'
222
+ )}
223
+ />
224
+ </div>
225
+ </div>
226
+
227
+ {/* Operations list */}
228
+ <div className="flex-1 overflow-y-auto py-2">
229
+ {!hasOperations ? (
230
+ <div className="flex flex-col items-center justify-center h-full px-4 text-center">
231
+ <FileText className="w-8 h-8 text-muted-foreground/50 mb-2" />
232
+ <p className="text-sm text-muted-foreground">
233
+ No schema loaded
234
+ </p>
235
+ <p className="text-xs text-muted-foreground/60 mt-1">
236
+ Operations will appear here
237
+ </p>
238
+ </div>
239
+ ) : !hasFilteredResults ? (
240
+ <div className="flex flex-col items-center justify-center h-32 px-4 text-center">
241
+ <p className="text-sm text-muted-foreground">
242
+ No matching operations
243
+ </p>
244
+ </div>
245
+ ) : (
246
+ <>
247
+ {renderSection(
248
+ 'Queries',
249
+ <MagnifyingGlass className="w-3.5 h-3.5" weight="bold" />,
250
+ filteredQueries,
251
+ 'query',
252
+ 'text-blue-500'
253
+ )}
254
+ {renderSection(
255
+ 'Mutations',
256
+ <Lightning className="w-3.5 h-3.5" weight="bold" />,
257
+ filteredMutations,
258
+ 'mutation',
259
+ 'text-orange-500'
260
+ )}
261
+ {renderSection(
262
+ 'Subscriptions',
263
+ <ArrowsClockwise className="w-3.5 h-3.5" weight="bold" />,
264
+ filteredSubscriptions,
265
+ 'subscription',
266
+ 'text-purple-500'
267
+ )}
268
+ </>
269
+ )}
270
+ </div>
271
+ </div>
272
+ )
273
+ }
274
+
275
+ // ============================================================================
276
+ // Main Component
277
+ // ============================================================================
278
+
279
+ export function GraphQLPlayground({
280
+ endpoint,
281
+ defaultQuery = `# Welcome to the GraphQL Playground!
282
+ #
283
+ # Select an operation from the sidebar, or
284
+ # type your query below and press Ctrl+Enter
285
+
286
+ query {
287
+ __typename
288
+ }
289
+ `,
290
+ headers: defaultHeaders = {},
291
+ operations = [],
292
+ selectedOperationId: externalSelectedId,
293
+ hideExplorer = false,
294
+ className,
295
+ theme = 'dark',
296
+ }: GraphQLPlaygroundProps) {
297
+ const [query, setQuery] = useState(defaultQuery)
298
+ const [variables, setVariables] = useState('{}')
299
+ const [customHeaders, setCustomHeaders] = useState(
300
+ Object.keys(defaultHeaders).length > 0
301
+ ? JSON.stringify(defaultHeaders, null, 2)
302
+ : '{}'
303
+ )
304
+ const [response, setResponse] = useState<GraphQLResponse | null>(null)
305
+ const [isLoading, setIsLoading] = useState(false)
306
+ const [error, setError] = useState<string | null>(null)
307
+ const [copied, setCopied] = useState(false)
308
+ const [activeTab, setActiveTab] = useState<TabType>('variables')
309
+ const [responseTime, setResponseTime] = useState<number | null>(null)
310
+ const [internalSelectedId, setInternalSelectedId] = useState<string | undefined>()
311
+
312
+ // Use external selection if provided, otherwise use internal
313
+ const selectedOperationId = externalSelectedId ?? internalSelectedId
314
+
315
+ const abortControllerRef = useRef<AbortController | null>(null)
316
+ const hasLoadedRef = useRef(false)
317
+
318
+ // Load from localStorage on mount
319
+ useEffect(() => {
320
+ const stored = loadStoredState()
321
+ if (stored) {
322
+ if (stored.query) setQuery(stored.query)
323
+ if (stored.variables) setVariables(stored.variables)
324
+ if (stored.headers) setCustomHeaders(stored.headers)
325
+ }
326
+ hasLoadedRef.current = true
327
+ }, [])
328
+
329
+ // Save to localStorage when values change
330
+ useEffect(() => {
331
+ if (!hasLoadedRef.current) return
332
+ const timeoutId = setTimeout(() => {
333
+ saveStoredState({ query, variables, headers: customHeaders })
334
+ }, 500)
335
+ return () => clearTimeout(timeoutId)
336
+ }, [query, variables, customHeaders])
337
+
338
+ // Handle operation selection from sidebar (internal)
339
+ const handleSelectOperation = useCallback((operation: GraphQLOperationItem) => {
340
+ setQuery(operation.query)
341
+ setInternalSelectedId(operation.id)
342
+
343
+ // Set example variables if available
344
+ if (operation.exampleVariables && Object.keys(operation.exampleVariables).length > 0) {
345
+ setVariables(JSON.stringify(operation.exampleVariables, null, 2))
346
+ } else {
347
+ setVariables('{}')
348
+ }
349
+
350
+ // Clear previous response
351
+ setResponse(null)
352
+ setError(null)
353
+ setResponseTime(null)
354
+ }, [])
355
+
356
+ // Sync with external selection (from main sidebar)
357
+ useEffect(() => {
358
+ if (externalSelectedId && operations.length > 0) {
359
+ const op = operations.find(o => o.id === externalSelectedId)
360
+ if (op) {
361
+ setQuery(op.query)
362
+ if (op.exampleVariables && Object.keys(op.exampleVariables).length > 0) {
363
+ setVariables(JSON.stringify(op.exampleVariables, null, 2))
364
+ } else {
365
+ setVariables('{}')
366
+ }
367
+ setResponse(null)
368
+ setError(null)
369
+ setResponseTime(null)
370
+ }
371
+ }
372
+ }, [externalSelectedId, operations])
373
+
374
+ // Execute GraphQL query
375
+ const executeQuery = useCallback(async () => {
376
+ setIsLoading(true)
377
+ setError(null)
378
+ setResponse(null)
379
+ setResponseTime(null)
380
+
381
+ if (abortControllerRef.current) {
382
+ abortControllerRef.current.abort()
383
+ }
384
+ abortControllerRef.current = new AbortController()
385
+
386
+ const startTime = performance.now()
387
+
388
+ try {
389
+ let parsedVariables = {}
390
+ try {
391
+ parsedVariables = variables.trim() && variables.trim() !== '{}'
392
+ ? JSON.parse(variables)
393
+ : {}
394
+ } catch {
395
+ setError('Invalid JSON in variables')
396
+ setIsLoading(false)
397
+ return
398
+ }
399
+
400
+ let parsedHeaders: Record<string, string> = {}
401
+ try {
402
+ parsedHeaders = customHeaders.trim() && customHeaders.trim() !== '{}'
403
+ ? JSON.parse(customHeaders)
404
+ : {}
405
+ } catch {
406
+ setError('Invalid JSON in headers')
407
+ setIsLoading(false)
408
+ return
409
+ }
410
+
411
+ const res = await fetch(endpoint, {
412
+ method: 'POST',
413
+ headers: {
414
+ 'Content-Type': 'application/json',
415
+ Accept: 'application/json',
416
+ ...defaultHeaders,
417
+ ...parsedHeaders,
418
+ },
419
+ body: JSON.stringify({
420
+ query,
421
+ variables: parsedVariables,
422
+ }),
423
+ signal: abortControllerRef.current.signal,
424
+ })
425
+
426
+ const endTime = performance.now()
427
+ setResponseTime(Math.round(endTime - startTime))
428
+
429
+ const data = await res.json()
430
+ setResponse(data)
431
+ } catch (err) {
432
+ if (err instanceof Error && err.name === 'AbortError') return
433
+ setError(err instanceof Error ? err.message : 'Request failed')
434
+ } finally {
435
+ setIsLoading(false)
436
+ }
437
+ }, [query, variables, customHeaders, endpoint, defaultHeaders])
438
+
439
+ const handleCancel = useCallback(() => {
440
+ if (abortControllerRef.current) {
441
+ abortControllerRef.current.abort()
442
+ abortControllerRef.current = null
443
+ }
444
+ setIsLoading(false)
445
+ }, [])
446
+
447
+ useEffect(() => {
448
+ const handleKeyDown = (e: KeyboardEvent) => {
449
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
450
+ e.preventDefault()
451
+ if (!isLoading) executeQuery()
452
+ }
453
+ }
454
+ window.addEventListener('keydown', handleKeyDown)
455
+ return () => window.removeEventListener('keydown', handleKeyDown)
456
+ }, [executeQuery, isLoading])
457
+
458
+ const copyResponse = useCallback(() => {
459
+ if (response) {
460
+ navigator.clipboard.writeText(JSON.stringify(response, null, 2))
461
+ setCopied(true)
462
+ setTimeout(() => setCopied(false), 2000)
463
+ }
464
+ }, [response])
465
+
466
+ const handleReset = useCallback(() => {
467
+ setQuery(defaultQuery)
468
+ setVariables('{}')
469
+ setCustomHeaders(
470
+ Object.keys(defaultHeaders).length > 0
471
+ ? JSON.stringify(defaultHeaders, null, 2)
472
+ : '{}'
473
+ )
474
+ setResponse(null)
475
+ setError(null)
476
+ setResponseTime(null)
477
+ setInternalSelectedId(undefined)
478
+ localStorage.removeItem(STORAGE_KEY)
479
+ }, [defaultQuery, defaultHeaders])
480
+
481
+ const monacoTheme = theme === 'dark' ? 'vs-dark' : 'vs'
482
+ const hasErrors = response?.errors && response.errors.length > 0
483
+ const statusColor = hasErrors ? 'text-red-400' : response ? 'text-emerald-400' : 'text-zinc-500'
484
+
485
+ return (
486
+ <TooltipProvider>
487
+ <div className={cn('flex flex-col h-full bg-background', className)}>
488
+ {/* Toolbar */}
489
+ <div className="flex items-center justify-between gap-2 px-4 py-2 border-b border-border bg-muted/50">
490
+ <div className="flex items-center gap-2">
491
+ <div className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-md bg-background border border-border">
492
+ <span className="text-xs font-medium text-emerald-500">POST</span>
493
+ <span className="text-xs text-muted-foreground font-mono truncate max-w-[200px] md:max-w-[400px]">
494
+ {endpoint}
495
+ </span>
496
+ </div>
497
+ </div>
498
+
499
+ <div className="flex items-center gap-2">
500
+ <Button
501
+ onClick={isLoading ? handleCancel : executeQuery}
502
+ variant={isLoading ? 'destructive' : 'default'}
503
+ size="sm"
504
+ className="px-4"
505
+ >
506
+ {isLoading ? (
507
+ <>
508
+ <X className="h-4 w-4 mr-2" weight="bold" />
509
+ Cancel
510
+ </>
511
+ ) : (
512
+ <>
513
+ <Play className="h-4 w-4 mr-2" weight="fill" />
514
+ Run
515
+ </>
516
+ )}
517
+ </Button>
518
+
519
+ <Tooltip>
520
+ <TooltipTrigger asChild>
521
+ <Button variant="outline" size="icon" onClick={handleReset}>
522
+ <ArrowClockwise className="h-4 w-4" weight="bold" />
523
+ </Button>
524
+ </TooltipTrigger>
525
+ <TooltipContent>Reset to defaults</TooltipContent>
526
+ </Tooltip>
527
+ </div>
528
+ </div>
529
+
530
+ {/* Main Content */}
531
+ <div className="flex-1 flex overflow-hidden">
532
+ {/* Left Sidebar - Schema Explorer (only shown when not using main sidebar) */}
533
+ {!hideExplorer && operations.length > 0 && (
534
+ <div className="w-64 flex-shrink-0 hidden md:block">
535
+ <SchemaExplorer
536
+ operations={operations}
537
+ onSelectOperation={handleSelectOperation}
538
+ selectedOperationId={selectedOperationId}
539
+ />
540
+ </div>
541
+ )}
542
+
543
+ {/* Center & Right - Query/Response */}
544
+ <div className="flex-1 flex flex-col lg:flex-row overflow-hidden">
545
+ {/* Query editor */}
546
+ <div className="flex-1 flex flex-col border-b lg:border-b-0 lg:border-r border-border min-h-[200px] lg:min-h-0">
547
+ <div className="flex-1 flex flex-col min-h-0">
548
+ <div className="flex items-center px-3 py-2 border-b border-border bg-muted/30">
549
+ <FileText className="h-4 w-4 mr-2 text-muted-foreground" />
550
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
551
+ Query
552
+ </span>
553
+ <span className="ml-auto text-xs text-muted-foreground">
554
+ Ctrl+Enter to run
555
+ </span>
556
+ </div>
557
+ <div className="flex-1 min-h-0">
558
+ <Editor
559
+ height="100%"
560
+ language="graphql"
561
+ value={query}
562
+ onChange={(value) => {
563
+ setQuery(value || '')
564
+ setInternalSelectedId(undefined) // Clear internal selection when manually editing
565
+ }}
566
+ theme={monacoTheme}
567
+ options={{
568
+ minimap: { enabled: false },
569
+ scrollBeyondLastLine: false,
570
+ fontSize: 13,
571
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace",
572
+ lineNumbers: 'on',
573
+ renderLineHighlight: 'line',
574
+ scrollbar: { vertical: 'auto', horizontal: 'auto' },
575
+ padding: { top: 12, bottom: 12 },
576
+ wordWrap: 'on',
577
+ automaticLayout: true,
578
+ tabSize: 2,
579
+ }}
580
+ loading={
581
+ <div className="flex items-center justify-center h-full text-muted-foreground text-sm">
582
+ <Spinner className="w-5 h-5 animate-spin mr-2" />
583
+ Loading editor...
584
+ </div>
585
+ }
586
+ />
587
+ </div>
588
+ </div>
589
+
590
+ {/* Variables/Headers tabs */}
591
+ <div className="border-t border-border h-[180px] flex flex-col">
592
+ <div className="flex border-b border-border bg-muted/30">
593
+ <button
594
+ onClick={() => setActiveTab('variables')}
595
+ className={cn(
596
+ 'flex items-center gap-1.5 px-4 py-2 text-xs font-medium uppercase tracking-wider transition-colors',
597
+ activeTab === 'variables'
598
+ ? 'text-primary border-b-2 border-primary -mb-px bg-background'
599
+ : 'text-muted-foreground hover:text-foreground'
600
+ )}
601
+ >
602
+ <BracketsCurly className="h-3.5 w-3.5" />
603
+ Variables
604
+ </button>
605
+ <button
606
+ onClick={() => setActiveTab('headers')}
607
+ className={cn(
608
+ 'flex items-center gap-1.5 px-4 py-2 text-xs font-medium uppercase tracking-wider transition-colors',
609
+ activeTab === 'headers'
610
+ ? 'text-primary border-b-2 border-primary -mb-px bg-background'
611
+ : 'text-muted-foreground hover:text-foreground'
612
+ )}
613
+ >
614
+ <Key className="h-3.5 w-3.5" />
615
+ Headers
616
+ </button>
617
+ </div>
618
+ <div className="flex-1 min-h-0">
619
+ <Editor
620
+ height="100%"
621
+ language="json"
622
+ value={activeTab === 'variables' ? variables : customHeaders}
623
+ onChange={(value) =>
624
+ activeTab === 'variables'
625
+ ? setVariables(value || '{}')
626
+ : setCustomHeaders(value || '{}')
627
+ }
628
+ theme={monacoTheme}
629
+ options={{
630
+ minimap: { enabled: false },
631
+ scrollBeyondLastLine: false,
632
+ fontSize: 13,
633
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace",
634
+ lineNumbers: 'off',
635
+ renderLineHighlight: 'none',
636
+ scrollbar: { vertical: 'auto', horizontal: 'auto' },
637
+ padding: { top: 8, bottom: 8 },
638
+ wordWrap: 'on',
639
+ automaticLayout: true,
640
+ tabSize: 2,
641
+ folding: false,
642
+ }}
643
+ loading={
644
+ <div className="flex items-center justify-center h-full text-muted-foreground text-sm">
645
+ Loading...
646
+ </div>
647
+ }
648
+ />
649
+ </div>
650
+ </div>
651
+ </div>
652
+
653
+ {/* Response */}
654
+ <div className="flex-1 flex flex-col min-h-[200px] lg:min-h-0">
655
+ <div className="flex items-center justify-between px-3 py-2 border-b border-border bg-muted/30">
656
+ <div className="flex items-center gap-3">
657
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
658
+ Response
659
+ </span>
660
+ {responseTime !== null && (
661
+ <span className="text-xs text-muted-foreground">
662
+ {responseTime}ms
663
+ </span>
664
+ )}
665
+ {response && (
666
+ <span className={cn('text-xs font-medium', statusColor)}>
667
+ {hasErrors ? 'Error' : 'Success'}
668
+ </span>
669
+ )}
670
+ </div>
671
+ {response && (
672
+ <Tooltip>
673
+ <TooltipTrigger asChild>
674
+ <Button variant="ghost" size="sm" onClick={copyResponse} className="h-7 px-2">
675
+ {copied ? (
676
+ <>
677
+ <Check className="h-3.5 w-3.5 mr-1 text-emerald-500" />
678
+ <span className="text-xs">Copied</span>
679
+ </>
680
+ ) : (
681
+ <>
682
+ <Copy className="h-3.5 w-3.5 mr-1" />
683
+ <span className="text-xs">Copy</span>
684
+ </>
685
+ )}
686
+ </Button>
687
+ </TooltipTrigger>
688
+ <TooltipContent>Copy response</TooltipContent>
689
+ </Tooltip>
690
+ )}
691
+ </div>
692
+ <div className="flex-1 min-h-0 overflow-hidden">
693
+ {isLoading ? (
694
+ <div className="flex items-center justify-center h-full">
695
+ <Spinner className="w-6 h-6 animate-spin text-primary" />
696
+ <span className="ml-2 text-sm text-muted-foreground">Executing query...</span>
697
+ </div>
698
+ ) : error ? (
699
+ <div className="p-4">
700
+ <div className="p-4 bg-destructive/10 border border-destructive/20 rounded-lg">
701
+ <p className="text-destructive text-sm font-mono">{error}</p>
702
+ </div>
703
+ </div>
704
+ ) : response ? (
705
+ <Editor
706
+ height="100%"
707
+ language="json"
708
+ value={JSON.stringify(response, null, 2)}
709
+ theme={monacoTheme}
710
+ options={{
711
+ readOnly: true,
712
+ minimap: { enabled: false },
713
+ scrollBeyondLastLine: false,
714
+ fontSize: 13,
715
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace",
716
+ lineNumbers: 'on',
717
+ renderLineHighlight: 'none',
718
+ scrollbar: { vertical: 'auto', horizontal: 'auto' },
719
+ padding: { top: 12, bottom: 12 },
720
+ wordWrap: 'on',
721
+ automaticLayout: true,
722
+ folding: true,
723
+ }}
724
+ />
725
+ ) : (
726
+ <div className="flex flex-col items-center justify-center h-full text-muted-foreground">
727
+ <Play className="h-12 w-12 mb-3 opacity-20" />
728
+ <p className="text-sm">Run a query to see results</p>
729
+ <p className="text-xs mt-1 opacity-60">Press Ctrl+Enter or click Run</p>
730
+ </div>
731
+ )}
732
+ </div>
733
+ </div>
734
+ </div>
735
+ </div>
736
+ </div>
737
+ </TooltipProvider>
738
+ )
739
+ }
740
+
741
+ export function GraphQLPlaygroundWithExplorer(props: GraphQLPlaygroundProps) {
742
+ return <GraphQLPlayground {...props} />
743
+ }
744
+
745
+ export default GraphQLPlayground