polen 0.10.0-next.21 → 0.10.0-next.22

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 (82) hide show
  1. package/build/api/config/configurator.d.ts +62 -11
  2. package/build/api/config/configurator.d.ts.map +1 -1
  3. package/build/api/config/configurator.js +9 -0
  4. package/build/api/config/configurator.js.map +1 -1
  5. package/build/api/vite/plugins/core.d.ts.map +1 -1
  6. package/build/api/vite/plugins/core.js +1 -0
  7. package/build/api/vite/plugins/core.js.map +1 -1
  8. package/build/project-data.d.ts +1 -0
  9. package/build/project-data.d.ts.map +1 -1
  10. package/build/sandbox.js +40 -17
  11. package/build/sandbox.js.map +1 -1
  12. package/build/template/components/CodeBlock.d.ts.map +1 -1
  13. package/build/template/components/CodeBlock.js +3 -5
  14. package/build/template/components/CodeBlock.js.map +1 -1
  15. package/build/template/components/Field.js +1 -1
  16. package/build/template/components/Field.js.map +1 -1
  17. package/build/template/components/GraphQLInteractive/GraphQLInteractive.d.ts +31 -0
  18. package/build/template/components/GraphQLInteractive/GraphQLInteractive.d.ts.map +1 -0
  19. package/build/template/components/GraphQLInteractive/GraphQLInteractive.js +275 -0
  20. package/build/template/components/GraphQLInteractive/GraphQLInteractive.js.map +1 -0
  21. package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.d.ts +39 -0
  22. package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.d.ts.map +1 -0
  23. package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.js +51 -0
  24. package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.js.map +1 -0
  25. package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.d.ts +33 -0
  26. package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.d.ts.map +1 -0
  27. package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.js +242 -0
  28. package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.js.map +1 -0
  29. package/build/template/components/GraphQLInteractive/hooks/use-popover-state.d.ts +45 -0
  30. package/build/template/components/GraphQLInteractive/hooks/use-popover-state.d.ts.map +1 -0
  31. package/build/template/components/GraphQLInteractive/hooks/use-popover-state.js +176 -0
  32. package/build/template/components/GraphQLInteractive/hooks/use-popover-state.js.map +1 -0
  33. package/build/template/components/GraphQLInteractive/index.d.ts +2 -0
  34. package/build/template/components/GraphQLInteractive/index.d.ts.map +1 -0
  35. package/build/template/components/GraphQLInteractive/index.js +2 -0
  36. package/build/template/components/GraphQLInteractive/index.js.map +1 -0
  37. package/build/template/components/GraphQLInteractive/lib/graphql-node-types.d.ts +52 -0
  38. package/build/template/components/GraphQLInteractive/lib/graphql-node-types.d.ts.map +1 -0
  39. package/build/template/components/GraphQLInteractive/lib/graphql-node-types.js +34 -0
  40. package/build/template/components/GraphQLInteractive/lib/graphql-node-types.js.map +1 -0
  41. package/build/template/components/GraphQLInteractive/lib/parser.d.ts +71 -0
  42. package/build/template/components/GraphQLInteractive/lib/parser.d.ts.map +1 -0
  43. package/build/template/components/GraphQLInteractive/lib/parser.js +836 -0
  44. package/build/template/components/GraphQLInteractive/lib/parser.js.map +1 -0
  45. package/build/template/components/GraphQLInteractive/lib/semantic-nodes.d.ts +98 -0
  46. package/build/template/components/GraphQLInteractive/lib/semantic-nodes.d.ts.map +1 -0
  47. package/build/template/components/GraphQLInteractive/lib/semantic-nodes.js +31 -0
  48. package/build/template/components/GraphQLInteractive/lib/semantic-nodes.js.map +1 -0
  49. package/build/template/components/content/$$.d.ts +0 -1
  50. package/build/template/components/content/$$.d.ts.map +1 -1
  51. package/build/template/components/content/$$.js +0 -1
  52. package/build/template/components/content/$$.js.map +1 -1
  53. package/package.json +5 -21
  54. package/src/api/config/configurator.ts +72 -11
  55. package/src/api/vite/plugins/core.ts +1 -0
  56. package/src/lib/kit-temp.test.ts +9 -9
  57. package/src/project-data.ts +1 -0
  58. package/src/sandbox.ts +40 -17
  59. package/src/template/components/CodeBlock.tsx +6 -9
  60. package/src/template/components/Field.tsx +1 -1
  61. package/src/template/components/GraphQLInteractive/GraphQLInteractive.tsx +464 -0
  62. package/src/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.tsx +96 -0
  63. package/src/template/components/GraphQLInteractive/components/GraphQLTokenPopover.tsx +492 -0
  64. package/src/template/components/GraphQLInteractive/hooks/use-popover-state.ts +244 -0
  65. package/src/template/components/GraphQLInteractive/index.ts +1 -0
  66. package/src/template/components/GraphQLInteractive/lib/graphql-node-types.ts +217 -0
  67. package/src/template/components/GraphQLInteractive/lib/parser.ts +1075 -0
  68. package/src/template/components/GraphQLInteractive/lib/semantic-nodes.ts +154 -0
  69. package/src/template/components/GraphQLInteractive/tests/parser-comment.test.ts +33 -0
  70. package/src/template/components/GraphQLInteractive/tests/parser-error-hint.test.ts +102 -0
  71. package/src/template/components/GraphQLInteractive/tests/parser.test.ts +131 -0
  72. package/src/template/components/content/$$.ts +0 -1
  73. package/build/template/components/content/GraphQLDocumentWithSchema.d.ts +0 -8
  74. package/build/template/components/content/GraphQLDocumentWithSchema.d.ts.map +0 -1
  75. package/build/template/components/content/GraphQLDocumentWithSchema.js +0 -13
  76. package/build/template/components/content/GraphQLDocumentWithSchema.js.map +0 -1
  77. package/build/template/components/content/GraphQLDocumentWrapper.d.ts +0 -7
  78. package/build/template/components/content/GraphQLDocumentWrapper.d.ts.map +0 -1
  79. package/build/template/components/content/GraphQLDocumentWrapper.js +0 -48
  80. package/build/template/components/content/GraphQLDocumentWrapper.js.map +0 -1
  81. package/src/template/components/content/GraphQLDocumentWithSchema.tsx +0 -13
  82. package/src/template/components/content/GraphQLDocumentWrapper.tsx +0 -72
@@ -0,0 +1,464 @@
1
+ /**
2
+ * Interactive GraphQL code block with tree-sitter parsing
3
+ *
4
+ * This component replaces CodeHike's default rendering for GraphQL code blocks
5
+ * that have the "interactive" meta flag. It provides:
6
+ * - Syntax highlighting using tree-sitter
7
+ * - Hover tooltips showing type information (when schema is available)
8
+ * - Click navigation to reference documentation
9
+ * - Integration with CodeHike's annotation system
10
+ */
11
+
12
+ import type { React } from '#dep/react/index'
13
+ import { React as ReactHooks } from '#dep/react/index'
14
+ import { Box } from '@radix-ui/themes'
15
+ import type { HighlightedCode } from 'codehike/code'
16
+ import type { GraphQLSchema } from 'graphql'
17
+ import { GraphQLErrorBoundary } from './components/GraphQLErrorBoundary.js'
18
+ import { GraphQLTokenPopover } from './components/GraphQLTokenPopover.js'
19
+ import { usePopoverState } from './hooks/use-popover-state.js'
20
+ import { type GraphQLToken, parseGraphQLWithTreeSitter } from './lib/parser.js'
21
+
22
+ interface GraphQLInteractiveProps {
23
+ /** The code block from CodeHike with code and annotations */
24
+ codeblock: HighlightedCode
25
+
26
+ /** The GraphQL schema for providing type information and validation */
27
+ schema?: GraphQLSchema
28
+
29
+ /** Whether to show a warning indicator when schema is missing */
30
+ showWarningIfNoSchema?: boolean
31
+ }
32
+
33
+ /**
34
+ * Main component that renders an interactive GraphQL code block
35
+ *
36
+ * This component:
37
+ * 1. Parses the GraphQL code into tokens using tree-sitter
38
+ * 2. Renders each token with appropriate styling
39
+ * 3. Adds interactivity to certain token types (types and fields)
40
+ * 4. Shows loading/error states during parsing
41
+ */
42
+ /**
43
+ * Internal GraphQL Interactive implementation
44
+ * Wrapped by error boundary in the main export
45
+ */
46
+ const GraphQLInteractiveImpl: React.FC<GraphQLInteractiveProps> = ({
47
+ codeblock,
48
+ schema,
49
+ showWarningIfNoSchema = true,
50
+ }) => {
51
+ // State to hold the parsed tokens
52
+ const [tokens, setTokens] = ReactHooks.useState<GraphQLToken[] | null>(null)
53
+
54
+ // Loading state while parser initializes and processes the code
55
+ const [isLoading, setIsLoading] = ReactHooks.useState(true)
56
+
57
+ // Error state if parsing fails
58
+ const [error, setError] = ReactHooks.useState<string | null>(null)
59
+
60
+ // Retry attempt counter
61
+ const [retryCount, setRetryCount] = ReactHooks.useState(0)
62
+
63
+ // Popover state management - must be called at top level for hooks rules
64
+ const popoverState = usePopoverState({
65
+ showDelay: 300,
66
+ hideDelay: 100,
67
+ allowMultiplePins: true,
68
+ })
69
+
70
+ // Memoize token parsing to avoid re-computation on unrelated renders
71
+ const parseTokens = ReactHooks.useCallback(async () => {
72
+ try {
73
+ setIsLoading(true)
74
+ setError(null)
75
+
76
+ // Parse the code into tokens with semantic analysis
77
+ const parsedTokens = await parseGraphQLWithTreeSitter(
78
+ codeblock.code,
79
+ codeblock.annotations,
80
+ schema, // Pass the schema for semantic analysis
81
+ )
82
+
83
+ setTokens(parsedTokens)
84
+ setRetryCount(0) // Reset retry count on success
85
+ } catch (err) {
86
+ // Provide detailed error information to users
87
+ const errorMessage = err instanceof Error ? err.message : 'Unknown parsing error'
88
+ setError(errorMessage)
89
+ setTokens([]) // Set empty tokens on error for fallback rendering
90
+ } finally {
91
+ setIsLoading(false)
92
+ }
93
+ }, [codeblock.code, codeblock.annotations, schema])
94
+
95
+ // Retry function for users
96
+ const handleRetry = ReactHooks.useCallback(() => {
97
+ setRetryCount(prev => prev + 1)
98
+ parseTokens()
99
+ }, [parseTokens])
100
+
101
+ // Parse the GraphQL code whenever dependencies change
102
+ ReactHooks.useEffect(() => {
103
+ parseTokens()
104
+ }, [parseTokens])
105
+
106
+ // Render loading state
107
+ // Shows the code with reduced opacity and a loading indicator
108
+ if (isLoading) {
109
+ return (
110
+ <div className='graphql-loading'>
111
+ <pre style={{ opacity: 0.5 }}>
112
+ <code>{codeblock.code}</code>
113
+ </pre>
114
+ <div
115
+ style={{
116
+ position: 'absolute',
117
+ top: '8px',
118
+ right: '8px',
119
+ fontSize: '12px',
120
+ color: '#666',
121
+ backgroundColor: '#f0f0f0',
122
+ padding: '2px 6px',
123
+ borderRadius: '3px',
124
+ }}
125
+ >
126
+ Loading tree-sitter...
127
+ </div>
128
+ </div>
129
+ )
130
+ }
131
+
132
+ // Render error state with retry option
133
+ if (error) {
134
+ return (
135
+ <Box
136
+ className='graphql-error'
137
+ p={'4'}
138
+ style={{
139
+ borderRadius: 'var(--radius-2)',
140
+ backgroundColor: 'var(--gray-2)',
141
+ position: 'relative',
142
+ borderLeft: '3px solid var(--red-9)',
143
+ }}
144
+ >
145
+ <pre style={{ margin: 0, whiteSpace: 'pre' }}>
146
+ <code>{codeblock.code}</code>
147
+ </pre>
148
+ <div
149
+ style={{
150
+ color: 'var(--red-11)',
151
+ fontSize: '12px',
152
+ marginTop: '8px',
153
+ padding: '8px',
154
+ backgroundColor: 'var(--red-a3)',
155
+ borderRadius: '3px',
156
+ display: 'flex',
157
+ justifyContent: 'space-between',
158
+ alignItems: 'center',
159
+ }}
160
+ >
161
+ <span>Interactive parsing failed: {error}</span>
162
+ {retryCount < 3 && (
163
+ <button
164
+ onClick={handleRetry}
165
+ style={{
166
+ backgroundColor: 'var(--red-9)',
167
+ color: 'white',
168
+ border: 'none',
169
+ padding: '4px 8px',
170
+ borderRadius: '3px',
171
+ fontSize: '11px',
172
+ cursor: 'pointer',
173
+ }}
174
+ >
175
+ Retry ({retryCount + 1}/3)
176
+ </button>
177
+ )}
178
+ </div>
179
+ </Box>
180
+ )
181
+ }
182
+
183
+ // Fallback if no tokens were parsed or parsing failed
184
+ if (!tokens || tokens.length === 0) {
185
+ return (
186
+ <Box
187
+ className='graphql-fallback'
188
+ p={'4'}
189
+ style={{
190
+ borderRadius: 'var(--radius-2)',
191
+ backgroundColor: 'var(--gray-2)',
192
+ position: 'relative',
193
+ }}
194
+ >
195
+ <pre style={{ margin: 0, whiteSpace: 'pre' }}>
196
+ <code>{codeblock.code}</code>
197
+ </pre>
198
+ {error && (
199
+ <div
200
+ style={{
201
+ position: 'absolute',
202
+ top: '8px',
203
+ right: '8px',
204
+ fontSize: '12px',
205
+ color: 'var(--red-11)',
206
+ backgroundColor: 'var(--red-a3)',
207
+ padding: '2px 6px',
208
+ borderRadius: '3px',
209
+ maxWidth: '200px',
210
+ }}
211
+ title={error}
212
+ >
213
+ Interactive features unavailable
214
+ </div>
215
+ )}
216
+ </Box>
217
+ )
218
+ }
219
+
220
+ // Main render: Show the parsed and interactive code
221
+ return (
222
+ <Box
223
+ className='graphql-interactive'
224
+ p={'4'}
225
+ position={'relative'}
226
+ style={{
227
+ borderRadius: 'var(--radius-2)',
228
+ backgroundColor: 'var(--gray-2)',
229
+ overflowX: 'auto',
230
+ maxWidth: '100%',
231
+ }}
232
+ >
233
+ {/* Render each token as a separate span with appropriate styling */}
234
+ <pre style={{ margin: 0, whiteSpace: 'pre' }}>
235
+ <code>
236
+ {tokens.map((token, index) => {
237
+ const tokenId = `${token.start}-${token.end}-${index}`
238
+ return (
239
+ <TokenComponent
240
+ key={tokenId}
241
+ token={token}
242
+ tokenId={tokenId}
243
+ popoverState={popoverState}
244
+ schema={schema}
245
+ />
246
+ )
247
+ })}
248
+ </code>
249
+ </pre>
250
+ {!schema && showWarningIfNoSchema && (
251
+ <div
252
+ style={{
253
+ position: 'absolute',
254
+ top: '8px',
255
+ right: '8px',
256
+ fontSize: '12px',
257
+ color: 'var(--amber-11)',
258
+ backgroundColor: 'var(--amber-a3)',
259
+ padding: '2px 6px',
260
+ borderRadius: '3px',
261
+ display: 'flex',
262
+ alignItems: 'center',
263
+ gap: '4px',
264
+ }}
265
+ title='Interactive features are not available because no GraphQL schema is configured'
266
+ >
267
+ <svg width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2'>
268
+ <path d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' />
269
+ <line x1='12' y1='9' x2='12' y2='13' />
270
+ <line x1='12' y1='17' x2='12.01' y2='17' />
271
+ </svg>
272
+ No schema configured
273
+ </div>
274
+ )}
275
+ </Box>
276
+ )
277
+ }
278
+
279
+ interface TokenComponentProps {
280
+ /** The token to render */
281
+ token: GraphQLToken
282
+
283
+ /** Unique ID for this token */
284
+ tokenId: string
285
+
286
+ /** Popover state manager */
287
+ popoverState: ReturnType<typeof usePopoverState>
288
+
289
+ /** The GraphQL schema for type information */
290
+ schema?: GraphQLSchema
291
+ }
292
+
293
+ /**
294
+ * Component that renders a single token with interactive features
295
+ *
296
+ * This component handles:
297
+ * - Applying syntax highlighting based on token type
298
+ * - Hover effects for interactive tokens
299
+ * - Click handlers for navigation
300
+ * - Visual feedback for CodeHike annotations
301
+ */
302
+ const TokenComponent: React.FC<TokenComponentProps> = ({ token, tokenId, popoverState, schema }) => {
303
+ // Track hover state for interactive tokens
304
+ const [isHovered, setIsHovered] = ReactHooks.useState(false)
305
+
306
+ // Handle clicks on interactive tokens - memoized to prevent unnecessary re-renders
307
+ const handleClick = ReactHooks.useCallback((e: React.MouseEvent) => {
308
+ if (token.polen.isInteractive()) {
309
+ e.preventDefault()
310
+ e.stopPropagation()
311
+
312
+ // Don't allow pinning for invalid fields
313
+ if (token.semantic && 'kind' in token.semantic && token.semantic.kind === 'InvalidField') {
314
+ return
315
+ }
316
+
317
+ // Toggle popover pin state only - no navigation
318
+ popoverState.onTogglePin(tokenId)
319
+ }
320
+ }, [token, tokenId, popoverState])
321
+
322
+ // Show hover effects when mouse enters an interactive token - memoized
323
+ const handleMouseEnter = ReactHooks.useCallback(() => {
324
+ if (token.polen.isInteractive()) {
325
+ setIsHovered(true)
326
+ popoverState.onHoverStart(tokenId)
327
+ }
328
+ }, [token, tokenId, popoverState])
329
+
330
+ // Hide hover effects when mouse leaves - memoized
331
+ const handleMouseLeave = ReactHooks.useCallback(() => {
332
+ setIsHovered(false)
333
+ popoverState.onHoverEnd(tokenId)
334
+ }, [tokenId, popoverState])
335
+
336
+ // Get the appropriate CSS class from the token
337
+ const baseClass = token.highlighter.getCssClass()
338
+
339
+ // Map class names to inline styles
340
+ const getBaseStyle = (): React.CSSProperties => {
341
+ switch (baseClass) {
342
+ case 'graphql-keyword':
343
+ return { color: 'var(--red-11)', fontWeight: 'bold' }
344
+ case 'graphql-type-interactive':
345
+ return { color: 'var(--blue-11)', fontWeight: 500 }
346
+ case 'graphql-field-interactive':
347
+ return { color: 'var(--violet-11)' }
348
+ case 'graphql-field-error':
349
+ return {
350
+ color: 'var(--red-11)',
351
+ }
352
+ case 'graphql-error-hint':
353
+ return {
354
+ color: 'var(--red-11)',
355
+ fontSize: '0.9em',
356
+ fontStyle: 'italic',
357
+ opacity: 0.5,
358
+ }
359
+ case 'graphql-comment':
360
+ return {
361
+ color: 'var(--gray-11)',
362
+ fontStyle: 'italic',
363
+ opacity: 0.6,
364
+ }
365
+ case 'graphql-operation':
366
+ return { color: 'var(--violet-11)', fontStyle: 'italic' }
367
+ case 'graphql-fragment':
368
+ return { color: 'var(--violet-11)', fontStyle: 'italic' }
369
+ case 'graphql-variable':
370
+ return { color: 'var(--orange-11)' }
371
+ case 'graphql-argument':
372
+ return { color: 'var(--gray-12)' }
373
+ case 'graphql-string':
374
+ return { color: 'var(--blue-11)' }
375
+ case 'graphql-number':
376
+ return { color: 'var(--blue-11)' }
377
+ case 'graphql-punctuation':
378
+ return { color: 'var(--gray-11)', opacity: 0.5 }
379
+ default:
380
+ return { color: 'var(--gray-12)' }
381
+ }
382
+ }
383
+
384
+ // Check if this is an invalid field
385
+ const isInvalidField = token.semantic && 'kind' in token.semantic && token.semantic.kind === 'InvalidField'
386
+
387
+ // Build the style object for this token
388
+ const style: React.CSSProperties = {
389
+ ...getBaseStyle(),
390
+ // Interactive tokens get special styling (except invalid fields)
391
+ ...(token.polen.isInteractive() && !isInvalidField && {
392
+ cursor: 'pointer',
393
+ textDecoration: isHovered ? 'underline' : 'none',
394
+ backgroundColor: isHovered ? 'var(--accent-a3)' : 'transparent',
395
+ }),
396
+
397
+ // Invalid fields get different hover styling - no cursor change, no underline
398
+ ...(isInvalidField && {
399
+ cursor: 'default',
400
+ textDecoration: 'underline wavy var(--red-a5)',
401
+ textUnderlineOffset: '2px',
402
+ // Subtle background change on hover to show it's interactive for popover
403
+ backgroundColor: isHovered ? 'var(--red-a2)' : 'transparent',
404
+ }),
405
+
406
+ // Tokens with CodeHike annotations get highlighted
407
+ ...(token.codeHike.annotations.length > 0 && {
408
+ position: 'relative',
409
+ backgroundColor: 'var(--yellow-a3)',
410
+ }),
411
+ }
412
+
413
+ // Build the span element
414
+ const tokenSpan = (
415
+ <span
416
+ className={baseClass}
417
+ style={style}
418
+ data-token-class={baseClass}
419
+ data-interactive={token.polen.isInteractive()}
420
+ >
421
+ {token.text}
422
+ </span>
423
+ )
424
+
425
+ // Wrap in popover if token has semantic information
426
+ return (
427
+ <GraphQLTokenPopover
428
+ token={token}
429
+ open={popoverState.isOpen(tokenId)}
430
+ pinned={popoverState.isPinned(tokenId)}
431
+ onTriggerHover={handleMouseEnter}
432
+ onTriggerLeave={handleMouseLeave}
433
+ onTriggerClick={handleClick}
434
+ onContentHover={() => popoverState.onPopoverHover(tokenId)}
435
+ onContentLeave={() => popoverState.onPopoverLeave(tokenId)}
436
+ onClose={() => popoverState.unpin(tokenId)}
437
+ >
438
+ {tokenSpan}
439
+ </GraphQLTokenPopover>
440
+ )
441
+ }
442
+
443
+ /**
444
+ * Main GraphQL Interactive component with error boundary protection
445
+ *
446
+ * This is the component that should be used in user code. It wraps the
447
+ * internal implementation with an error boundary that provides graceful
448
+ * fallback to static code rendering if interactive features fail.
449
+ */
450
+ export const GraphQLInteractive: React.FC<GraphQLInteractiveProps> = (props) => {
451
+ return (
452
+ <GraphQLErrorBoundary
453
+ fallbackCode={props.codeblock.code}
454
+ onError={(error, errorInfo) => {
455
+ // Log error for debugging (only in development)
456
+ if (process.env['NODE_ENV'] === 'development') {
457
+ console.error('GraphQL Interactive Error Boundary:', error, errorInfo)
458
+ }
459
+ }}
460
+ >
461
+ <GraphQLInteractiveImpl {...props} />
462
+ </GraphQLErrorBoundary>
463
+ )
464
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Error boundary for GraphQL Interactive components
3
+ *
4
+ * Provides graceful fallback rendering when the interactive GraphQL parser
5
+ * encounters errors. Falls back to static syntax highlighting.
6
+ */
7
+
8
+ import type { React } from '#dep/react/index'
9
+ import { React as ReactHooks } from '#dep/react/index'
10
+ import { Box } from '@radix-ui/themes'
11
+
12
+ interface GraphQLErrorBoundaryProps {
13
+ /** Child components to protect */
14
+ children: React.ReactNode
15
+ /** Fallback code to display if interactive parsing fails */
16
+ fallbackCode: string
17
+ /** Optional callback when errors occur */
18
+ onError?: (error: Error, errorInfo: React.ErrorInfo) => void
19
+ }
20
+
21
+ interface GraphQLErrorBoundaryState {
22
+ hasError: boolean
23
+ error?: Error
24
+ }
25
+
26
+ /**
27
+ * Error boundary that catches React errors in GraphQL Interactive components
28
+ * and provides a fallback static code display.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * <GraphQLErrorBoundary fallbackCode={codeblock.code}>
33
+ * <GraphQLInteractive codeblock={codeblock} schema={schema} />
34
+ * </GraphQLErrorBoundary>
35
+ * ```
36
+ */
37
+ export class GraphQLErrorBoundary extends ReactHooks.Component<
38
+ GraphQLErrorBoundaryProps,
39
+ GraphQLErrorBoundaryState
40
+ > {
41
+ constructor(props: GraphQLErrorBoundaryProps) {
42
+ super(props)
43
+ this.state = { hasError: false }
44
+ }
45
+
46
+ static getDerivedStateFromError(error: Error): GraphQLErrorBoundaryState {
47
+ // Update state so the next render will show the fallback UI
48
+ return { hasError: true, error }
49
+ }
50
+
51
+ override componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
52
+ // Log the error or send to error reporting service
53
+ console.error('GraphQL Interactive Error:', error, errorInfo)
54
+
55
+ // Call optional error handler
56
+ this.props.onError?.(error, errorInfo)
57
+ }
58
+
59
+ override render() {
60
+ if (this.state.hasError) {
61
+ return (
62
+ <Box
63
+ className='graphql-error-fallback'
64
+ p={'4'}
65
+ style={{
66
+ borderRadius: 'var(--radius-2)',
67
+ backgroundColor: 'var(--gray-2)',
68
+ position: 'relative',
69
+ borderLeft: '3px solid var(--red-9)',
70
+ }}
71
+ >
72
+ <pre style={{ margin: 0, whiteSpace: 'pre' }}>
73
+ <code>{this.props.fallbackCode}</code>
74
+ </pre>
75
+ <div
76
+ style={{
77
+ position: 'absolute',
78
+ top: '8px',
79
+ right: '8px',
80
+ fontSize: '12px',
81
+ color: 'var(--red-11)',
82
+ backgroundColor: 'var(--red-a3)',
83
+ padding: '2px 6px',
84
+ borderRadius: '3px',
85
+ }}
86
+ title={this.state.error?.message || 'Interactive features failed to load'}
87
+ >
88
+ Interactive mode unavailable
89
+ </div>
90
+ </Box>
91
+ )
92
+ }
93
+
94
+ return this.props.children
95
+ }
96
+ }